diff --git a/.eslintrc b/.eslintrc index 44a8d5ac..c9869cd0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -39,6 +39,12 @@ "message": "Use `globalThis` instead" } ], + "prefer-rest-params": 0, + "prefer-const": [ + "error", { + "destructuring": "all" + } + ], "require-yield": 0, "eqeqeq": ["error", "smart"], "spaced-comment": [ diff --git a/.npmignore b/.npmignore index a0225b11..0690250b 100644 --- a/.npmignore +++ b/.npmignore @@ -10,6 +10,7 @@ /tests /tmp /docs +/images /benches /build /builds diff --git a/Cargo.lock b/Cargo.lock index 5df6575f..02b76dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,11 +10,11 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bindgen" -version = "0.60.1" +version = "0.66.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" dependencies = [ - "bitflags", + "bitflags 2.4.0", "cexpr", "clang-sys", "lazy_static", @@ -25,6 +25,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", + "syn 2.0.13", ] [[package]] @@ -33,27 +34,35 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "boring" -version = "2.1.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c713ad6d8d7a681a43870ac37b89efd2a08015ceb4b256d82707509c1f0b6bb" +checksum = "7ae1aba472e42d3cf45ac6d0a6c8fc3ddf743871209e1b40229aed9fbdf48ece" dependencies = [ - "bitflags", + "bitflags 2.4.0", "boring-sys", "foreign-types", - "lazy_static", "libc", + "once_cell", ] [[package]] name = "boring-sys" -version = "2.1.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7663d3069437a5ccdb2b5f4f481c8b80446daea10fa8503844e89ac65fcdc363" +checksum = "ceced5be0047c7c48d77599535fd7f0a81c1b0f0a1e97e7eece24c45022bb481" dependencies = [ "bindgen", "cmake", + "fs_extra", + "fslock", ] [[package]] @@ -119,6 +128,12 @@ dependencies = [ "syn 1.0.103", ] +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "foreign-types" version = "0.5.0" @@ -146,6 +161,22 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "glob" version = "0.3.1" @@ -161,6 +192,15 @@ dependencies = [ "libc", ] +[[package]] +name = "intrusive-collections" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b694dc9f70c3bda874626d2aed13b780f137aab435f4e9814121955cf706122e" +dependencies = [ + "memoffset", +] + [[package]] name = "itoa" version = "1.0.4" @@ -190,9 +230,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libloading" @@ -225,6 +265,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -237,7 +286,7 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "466b16c759694cb07fbb023b0bde55afcc2ae35e8c0264b070c86a3e9a18cb6c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "ctor", "napi-sys", "once_cell", @@ -315,9 +364,9 @@ checksum = "3a74f2cda724d43a0a63140af89836d4e7db6138ef67c9f96d3a0f0150d05000" [[package]] name = "once_cell" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "peeking_take_while" @@ -354,18 +403,20 @@ dependencies = [ [[package]] name = "quiche" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8f75b639dc0dc928f02d8b5eb2e5fa3c212e3f4988eb7cf1cf0abdd93c8e67e" +checksum = "1442ef26c02f927e0fdf0fb590acf2cd2c5dbe202bdf1470ef6117f350d72783" dependencies = [ "boring", "cmake", + "either", "foreign-types-shared", - "lazy_static", + "intrusive-collections", "libc", "libm", "log", "octets", + "once_cell", "ring", "slab", "smallvec", diff --git a/Cargo.toml b/Cargo.toml index 407a823d..bccf8202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ path = "src/native/napi/lib.rs" napi = { version = "2", features = ["async", "napi6", "serde-json"] } serde = { version = "1.0", features = ["derive"] } napi-derive = { version = "2", default-features = false, features = ["strict", "compat-mode"] } -quiche = { version = "0.17.1", features = ["boringssl-boring-crate", "boringssl-vendored"] } -boring = "2.1.0" +quiche = { version = "0.18.0", features = ["boringssl-boring-crate", "boringssl-vendored"] } +boring = "3" [build-dependencies] napi-build = "2" diff --git a/README.md b/README.md index b1bb2a54..9a0586bb 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,57 @@ x86_64-apple-darwin The available target list is in `rustc --print target-list`. +### Structure + +It is possible to structure the QUIC system in the encapsulated way or the injected way. + +When using the encapsulated way, the `QUICSocket` is separated between client and server. + +When using the injected way, the `QUICSocket` is shared between client and server. + +![](/images/quic_structure_encapsulated.svg) + +If you are building a peer to peer network, you must use the injected way. This is the only way to ensure that hole-punching works because both the client and server for any given peer must share the same UDP socket and thus share the `QUICSocket`. When done in this way, the `QUICSocket` lifecycle is managed outside of both the `QUICClient` and `QUICServer`. + +![](/images/quic_structure_injected.svg) + +This also means both `QUICClient` and `QUICServer` must share the same connection map. In order to allow the `QUICSocket` to dispatch data into the correct connection, the connection map is constructed in the `QUICSocket`, however setting and unsetting connections is managed by `QUICClient` and `QUICServer`. + +## Dataflow + +The data flow of the QUIC system is a bidirectional graph. + +Data received from the outside world is received on the UDP socket. It is parsed and then dispatched to each `QUICConnection`. Each connection further parses the data and then dispatches to the `QUICStream`. Each `QUICStream` presents the data on the `ReadableStream` interface, which can be read by a caller. + +Data sent to the outside world is written to a `WritableStream` interface of a `QUICStream`. This data is buffered up in the underlying Quiche stream. A send procedure is triggered on the associated `QUICConnection` which takes all the buffered data to be sent for that connection, and sends it to the `QUICSocket`, which then sends it to the underlying UDP socket. + +![](/images/quic_dataflow.svg) + +Buffering occurs at the connection level and at the stream level. Each connection has a global buffer for all streams, and each stream has its own buffer. Note that connection buffering and stream buffering all occur within the Quiche library. The web streams `ReadableStream` and `WritableStream` do not do any buffering at all. + +### Connection Negotiation + +The connection negotiation process involves several exchanges of QUIC packets before the `QUICConnection` is constructed. + +The primary reason to do this is for both sides to determine their respective connection IDs. + +![](/images/quic_connection_negotiation.svg) + +### Push & Pull + +The `QUICSocket`, `QUICClient`, `QUICServer`, `QUICConnection` and `QUICStream` are independent state machines that exposes methods that can be called as well as events that may be emitted between them. + +This creates a concurrent decentralised state machine system where there are multiple entrypoints of change. + +Users may call methods which causes state transitions internally that trigger event emissions. However some methods are considered internal to the library, this means these methods are not intended to be called by the end user. They are however public relative to the other components in the system. These methods should be marked with `@internal` documentation annotation. + +External events may also trigger event handlers that will call methods which perform state transitions and event emission. + +Keeping track of how the system works is therefore quite complex and must follow a set of rules. + +* Pull methods - these are either synchronous or asynchronous methods that may throw exceptions. +* Push handlers - these are event handlers that can initiate pull methods, if these pull handlers throw exceptions, these exceptions must be caught, and expected runtime exceptions are to be converted to error events, all other exceptions will be considered to be software bugs and will be bubbled up to the program boundary as unhandled exceptions or unhandled promise rejections. Generally the only exceptions that are expected runtime exceptions are those that arise from perform IO with the operating system. + ## Benchmarks ```sh diff --git a/benches/index.ts b/benches/index.ts index 6c712d48..553f154f 100644 --- a/benches/index.ts +++ b/benches/index.ts @@ -1,35 +1,43 @@ #!/usr/bin/env ts-node +import type { Summary } from 'benny/lib/internal/common-types'; import fs from 'fs'; import path from 'path'; import si from 'systeminformation'; -import Stream1KB from './stream_1KB'; +import { fsWalk, resultsPath, suitesPath } from './utils'; async function main(): Promise { await fs.promises.mkdir(path.join(__dirname, 'results'), { recursive: true }); - // Running benches - await Stream1KB(); - const resultFilenames = await fs.promises.readdir( - path.join(__dirname, 'results'), - ); - const metricsFile = await fs.promises.open( - path.join(__dirname, 'results', 'metrics.txt'), - 'w', - ); + // Running all suites + for await (const suitePath of fsWalk(suitesPath)) { + // Skip over non-ts and non-js files + const ext = path.extname(suitePath); + if (ext !== '.ts' && ext !== '.js') { + continue; + } + const suite: () => Promise = (await import(suitePath)).default; + // Skip default exports that are not functions and are not called "main" + // They might be utility files + if (typeof suite === 'function' && suite.name === 'main') { + await suite(); + } + } + // Concatenating metrics + const metricsPath = path.join(resultsPath, 'metrics.txt'); + await fs.promises.rm(metricsPath, { force: true }); let concatenating = false; - for (const resultFilename of resultFilenames) { - if (/.+_metrics\.txt$/.test(resultFilename)) { - const metricsData = await fs.promises.readFile( - path.join(__dirname, 'results', resultFilename), - ); - if (concatenating) { - await metricsFile.write('\n'); - } - await metricsFile.write(metricsData); - concatenating = true; + for await (const metricPath of fsWalk(resultsPath)) { + // Skip over non-metrics files + if (!metricPath.endsWith('_metrics.txt')) { + continue; + } + const metricData = await fs.promises.readFile(metricPath); + if (concatenating) { + await fs.promises.appendFile(metricsPath, '\n'); } + await fs.promises.appendFile(metricsPath, metricData); + concatenating = true; } - await metricsFile.close(); const systemData = await si.get({ cpu: '*', osInfo: 'platform, distro, release, kernel, arch', @@ -41,4 +49,8 @@ async function main(): Promise { ); } -void main(); +if (require.main === module) { + void main(); +} + +export default main; diff --git a/benches/results/buffers/buffer_allocation.chart.html b/benches/results/buffers/buffer_allocation.chart.html new file mode 100644 index 00000000..96a325ca --- /dev/null +++ b/benches/results/buffers/buffer_allocation.chart.html @@ -0,0 +1,116 @@ + + + + + + + + buffers.buffer_allocation + + + +
+ +
+ + + \ No newline at end of file diff --git a/benches/results/buffers/buffer_allocation.json b/benches/results/buffers/buffer_allocation.json new file mode 100644 index 00000000..9afe9a5d --- /dev/null +++ b/benches/results/buffers/buffer_allocation.json @@ -0,0 +1,725 @@ +{ + "name": "buffers.buffer_allocation", + "date": "2023-09-25T09:52:51.016Z", + "version": "0.0.21", + "results": [ + { + "name": "Buffer.alloc", + "ops": 737442, + "margin": 2.08, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 78, + "promise": false, + "details": { + "min": 0.0000010986625032753952, + "max": 0.0000016742500000000001, + "mean": 0.0000013560393035203982, + "median": 0.0000013617432210932147, + "standardDeviation": 1.2738999378413602e-7, + "marginOfError": 2.8271189810953598e-8, + "relativeMarginOfError": 2.0848355750131344, + "standardErrorOfMean": 1.442407643416e-8, + "sampleVariance": 1.622821051632221e-14, + "sampleResults": [ + 0.0000010986625032753952, + 0.0000011134788846187439, + 0.0000011276247488863658, + 0.0000011546336142894577, + 0.00000117238723906018, + 0.000001173389881212333, + 0.0000011941693597694122, + 0.0000011995967299621888, + 0.0000012020611843829155, + 0.000001204891890121408, + 0.0000012050161367805048, + 0.0000012057989125687833, + 0.0000012092430561621102, + 0.000001217325181238536, + 0.0000012176456677439077, + 0.0000012185901170407896, + 0.000001227482160013975, + 0.000001232856670960236, + 0.0000012485665997030308, + 0.000001249308913442222, + 0.000001266167547384051, + 0.0000012725779075082063, + 0.0000012740726919381605, + 0.000001275355140186916, + 0.0000012797398096979267, + 0.0000012835547427722946, + 0.0000012897580574722684, + 0.0000012952652415058084, + 0.0000012986916542929514, + 0.0000013009723556642501, + 0.0000013052178792907677, + 0.0000013101953882435147, + 0.0000013169705869508253, + 0.0000013260327976242465, + 0.0000013376242774566473, + 0.0000013446694034413485, + 0.0000013501533012008143, + 0.0000013518121233295485, + 0.0000013605350272156894, + 0.0000013629514149707398, + 0.000001363685387370076, + 0.0000013685821687483623, + 0.0000013690103284129617, + 0.0000013802184111023393, + 0.00000139234376364748, + 0.000001393033561883134, + 0.0000014039616997117653, + 0.0000014043215814185398, + 0.0000014068259857896703, + 0.0000014098598349200805, + 0.0000014117684077211984, + 0.0000014262958118612978, + 0.0000014351908900340643, + 0.0000014417260677788454, + 0.0000014418666223459509, + 0.0000014458831994060618, + 0.0000014460282557428599, + 0.0000014499764407695186, + 0.000001455174927941305, + 0.0000014651181544239673, + 0.0000014704310638483711, + 0.0000014715286487902872, + 0.0000014792209144903484, + 0.0000014806151628963229, + 0.0000014856771115381256, + 0.0000014873990959909165, + 0.0000014914614813520832, + 0.0000014941578740501353, + 0.0000015062805085802137, + 0.0000015064176783998604, + 0.000001524117717704603, + 0.000001526004653675157, + 0.0000015317686666389664, + 0.0000015374989737094941, + 0.0000015510952703292862, + 0.000001580494409992139, + 0.000001586728142195825, + 0.0000016742500000000001 + ] + }, + "completed": true, + "percentSlower": 76.3 + }, + { + "name": "Buffer.allocUnsafe", + "ops": 3112051, + "margin": 2.56, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 80, + "promise": false, + "details": { + "min": 2.555665962268774e-7, + "max": 4.6726219935481236e-7, + "mean": 3.213315123386386e-7, + "median": 3.281196965984728e-7, + "standardDeviation": 3.755393669143905e-8, + "marginOfError": 8.229369215948564e-9, + "relativeMarginOfError": 2.5610215307099904, + "standardErrorOfMean": 4.198657763239063e-9, + "sampleVariance": 1.410298161024612e-15, + "sampleResults": [ + 2.555665962268774e-7, + 2.568863673486055e-7, + 2.5939856670341787e-7, + 2.6068248499326227e-7, + 2.6205789333986687e-7, + 2.623561445955327e-7, + 2.6288242680387116e-7, + 2.6559765098615704e-7, + 2.6728480644370943e-7, + 2.673137020703173e-7, + 2.6851988647964393e-7, + 2.7446904732737144e-7, + 2.764359406264037e-7, + 2.778849381354894e-7, + 2.7797970006941893e-7, + 2.826144646576014e-7, + 2.877922675456668e-7, + 2.8802015700926945e-7, + 2.8847494283147536e-7, + 2.8930581077218343e-7, + 2.924928539344195e-7, + 2.9570827038410474e-7, + 2.957527583207435e-7, + 2.9805790756764176e-7, + 3.0224364212791284e-7, + 3.0521817058096413e-7, + 3.064594206381907e-7, + 3.071455194168811e-7, + 3.0728278512801667e-7, + 3.103832463489447e-7, + 3.195901680346278e-7, + 3.2052644044264775e-7, + 3.21491603372943e-7, + 3.231656662174854e-7, + 3.237120492874352e-7, + 3.2592880987382087e-7, + 3.2608340479398913e-7, + 3.2719309179631674e-7, + 3.272848421740373e-7, + 3.2777849238433577e-7, + 3.2846090081260974e-7, + 3.3064135428151414e-7, + 3.310469445465311e-7, + 3.31358390501858e-7, + 3.313730093103026e-7, + 3.3239596349381354e-7, + 3.324848503409694e-7, + 3.3279659234758467e-7, + 3.335163389685165e-7, + 3.3392970313201846e-7, + 3.3417103087100325e-7, + 3.344453479113071e-7, + 3.3545746049246597e-7, + 3.3738512699579404e-7, + 3.3805868961574584e-7, + 3.389525143942178e-7, + 3.4186636346931276e-7, + 3.4213086487810854e-7, + 3.435780707664666e-7, + 3.4618569561844094e-7, + 3.469455827106047e-7, + 3.474986116215444e-7, + 3.4791550287884353e-7, + 3.5089916391032715e-7, + 3.5098313528523006e-7, + 3.528074492629344e-7, + 3.5488302316531613e-7, + 3.5577786455143127e-7, + 3.5677253052390873e-7, + 3.5839274776430237e-7, + 3.595908009718649e-7, + 3.6313423884192906e-7, + 3.6440725938176323e-7, + 3.6554783780472866e-7, + 3.6603361713422354e-7, + 3.715384713544857e-7, + 3.7290140471231984e-7, + 3.730704704152885e-7, + 3.749009249050594e-7, + 4.6726219935481236e-7 + ] + }, + "completed": true, + "percentSlower": 0 + }, + { + "name": "Buffer.allocUnsafeSlow", + "ops": 949633, + "margin": 3.18, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 79, + "promise": false, + "details": { + "min": 8.49354168058246e-7, + "max": 0.0000016337946195978892, + "mean": 0.0000010530378343278017, + "median": 0.000001013801983835415, + "standardDeviation": 1.519538036092758e-7, + "marginOfError": 3.350843164433514e-8, + "relativeMarginOfError": 3.1820729086837582, + "standardErrorOfMean": 1.709613859404854e-8, + "sampleVariance": 2.3089958431326352e-14, + "sampleResults": [ + 8.49354168058246e-7, + 8.766360129583863e-7, + 8.879558980696012e-7, + 8.915407621401376e-7, + 9.013284683721862e-7, + 9.018478057577983e-7, + 9.099482332509518e-7, + 9.1188529490348e-7, + 9.138630018034868e-7, + 9.322221294502705e-7, + 9.344896132522878e-7, + 9.369864738494423e-7, + 9.371619965266182e-7, + 9.393711007948701e-7, + 9.394758366174604e-7, + 9.567735455213413e-7, + 9.595262340525016e-7, + 9.725024380468906e-7, + 9.73217837819785e-7, + 9.80300881704629e-7, + 9.834223164785252e-7, + 9.837289927192573e-7, + 9.853895030392092e-7, + 9.854031961792799e-7, + 9.856682252354552e-7, + 9.86026584730479e-7, + 9.92545972212945e-7, + 9.956612116759067e-7, + 9.96878197849175e-7, + 9.973220559748848e-7, + 9.9777762006546e-7, + 9.991615623538842e-7, + 0.0000010019064023779307, + 0.0000010035918275332309, + 0.0000010053246109144346, + 0.000001008446880635896, + 0.000001009002504842696, + 0.000001010881520940485, + 0.0000010127290595150624, + 0.000001013801983835415, + 0.0000010146236390354685, + 0.0000010165807728274663, + 0.0000010166453476721662, + 0.0000010184084062520874, + 0.0000010238414935542047, + 0.0000010244829503707168, + 0.0000010283983868813037, + 0.0000010288581424086568, + 0.0000010305991249749516, + 0.0000010322059481664551, + 0.0000010380450704695746, + 0.0000010384034299645983, + 0.0000010389983802017232, + 0.0000010441503907554606, + 0.000001044213963663082, + 0.0000010445313272326496, + 0.0000010448828568565894, + 0.0000010469363602965734, + 0.0000010608262307127112, + 0.0000010640809899138334, + 0.0000010644405350343998, + 0.0000010736194142007882, + 0.0000010859377964063856, + 0.0000010924198784316347, + 0.000001101898069601229, + 0.0000011167566461826198, + 0.0000011381145548059582, + 0.0000011387847672166188, + 0.00000120224629283281, + 0.0000013097981764745175, + 0.0000013152911462160177, + 0.0000013272670496292832, + 0.000001331545204061185, + 0.000001349637799746176, + 0.0000013570409124306994, + 0.000001388655483935609, + 0.0000014207995624874758, + 0.0000015811698283347807, + 0.0000016337946195978892 + ] + }, + "completed": true, + "percentSlower": 69.49 + }, + { + "name": "Buffer.from subarray", + "ops": 1700664, + "margin": 2.07, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 86, + "promise": false, + "details": { + "min": 5.23711535652428e-7, + "max": 7.840166315394086e-7, + "mean": 5.880056422978829e-7, + "median": 5.691842250787755e-7, + "standardDeviation": 5.764372235900943e-8, + "marginOfError": 1.218312958189243e-8, + "relativeMarginOfError": 2.071940931430803, + "standardErrorOfMean": 6.215882439741035e-9, + "sampleVariance": 3.3227987274025636e-15, + "sampleResults": [ + 5.23711535652428e-7, + 5.246918782997016e-7, + 5.262428684710253e-7, + 5.300189165501554e-7, + 5.307825732640469e-7, + 5.311219820120615e-7, + 5.315089283827476e-7, + 5.327473028526116e-7, + 5.330040005503403e-7, + 5.33563625550385e-7, + 5.337087185158908e-7, + 5.368683145176436e-7, + 5.376556833121179e-7, + 5.396200204503245e-7, + 5.408270555737826e-7, + 5.419994814101411e-7, + 5.420723169382942e-7, + 5.422433484276204e-7, + 5.433639636067695e-7, + 5.444721520836376e-7, + 5.459007165006826e-7, + 5.468042482034565e-7, + 5.481619540493729e-7, + 5.491011039001691e-7, + 5.491712505999458e-7, + 5.50462166899689e-7, + 5.520176560089601e-7, + 5.529269109576177e-7, + 5.533338766179475e-7, + 5.533669605342534e-7, + 5.555397842282089e-7, + 5.561964796227124e-7, + 5.566552972600739e-7, + 5.586677532358949e-7, + 5.603201101813401e-7, + 5.603293023935227e-7, + 5.618527054944596e-7, + 5.647247240249578e-7, + 5.654906512802321e-7, + 5.662959036747981e-7, + 5.672048371277727e-7, + 5.687992216356086e-7, + 5.690258550531083e-7, + 5.693425951044427e-7, + 5.700386991089502e-7, + 5.716466154433735e-7, + 5.721669518582667e-7, + 5.722476471692995e-7, + 5.74253027341556e-7, + 5.754838432815025e-7, + 5.777054832939981e-7, + 5.791321758727906e-7, + 5.794679472465099e-7, + 5.816920452411261e-7, + 5.821318315561027e-7, + 5.823931679222052e-7, + 5.830948122952359e-7, + 5.897455917030112e-7, + 5.929155380730786e-7, + 5.940943845078358e-7, + 5.944211584662441e-7, + 6.038626385092131e-7, + 6.04300129379604e-7, + 6.051876317272177e-7, + 6.070704284134304e-7, + 6.109933790336064e-7, + 6.131824877725757e-7, + 6.198340300782118e-7, + 6.325533482189437e-7, + 6.342940881868075e-7, + 6.583402579245008e-7, + 6.590591494334425e-7, + 6.683847895494668e-7, + 6.703480728699317e-7, + 6.720556266999694e-7, + 6.745510423405187e-7, + 6.753918073900686e-7, + 6.771127562521828e-7, + 6.878596231297343e-7, + 6.936727530727655e-7, + 6.962360760418188e-7, + 7.002782181675787e-7, + 7.044072094573857e-7, + 7.066620487474468e-7, + 7.542833622900637e-7, + 7.840166315394086e-7 + ] + }, + "completed": true, + "percentSlower": 45.35 + }, + { + "name": "Buffer.copyBytesFrom", + "ops": 517646, + "margin": 1.49, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 85, + "promise": false, + "details": { + "min": 0.0000017095953339245515, + "max": 0.0000023522142276147225, + "mean": 0.000001931822168965275, + "median": 0.000001895428794143564, + "standardDeviation": 1.3568045392209921e-7, + "marginOfError": 2.8844558526636785e-8, + "relativeMarginOfError": 1.493127006720631, + "standardErrorOfMean": 1.4716611493182034e-8, + "sampleVariance": 1.840918557650689e-14, + "sampleResults": [ + 0.0000017095953339245515, + 0.0000017140713554812744, + 0.000001731756736475885, + 0.0000017453406098642473, + 0.0000017738836249125261, + 0.0000017822030152124974, + 0.0000017872606588490477, + 0.0000017873934115095235, + 0.0000017917062555426701, + 0.0000017933129194062224, + 0.000001798546387884576, + 0.000001807710280373832, + 0.0000018161507151087916, + 0.0000018226518862132478, + 0.0000018257173456246187, + 0.000001834719684132041, + 0.0000018393423371517657, + 0.0000018455790981649499, + 0.0000018473633498271537, + 0.0000018474268284416728, + 0.0000018489215840098232, + 0.0000018507548295261981, + 0.0000018511578661967058, + 0.0000018544002914661424, + 0.000001854836482957669, + 0.0000018557089405544634, + 0.000001857468995156559, + 0.0000018636433267809937, + 0.0000018638555886938251, + 0.0000018649798346099097, + 0.0000018676252061572293, + 0.000001868258557581509, + 0.0000018704880278327308, + 0.0000018768737544906121, + 0.0000018775543352206835, + 0.000001879120165086295, + 0.0000018824469260489392, + 0.0000018883146256123163, + 0.0000018908716193316613, + 0.0000018918432362371239, + 0.000001892464786823019, + 0.0000018951822680132854, + 0.000001895428794143564, + 0.0000018993890442731428, + 0.000001899589371653223, + 0.0000019037913644682437, + 0.000001904036873856165, + 0.000001908099495190668, + 0.000001913184843760591, + 0.0000019132952111330922, + 0.000001918831661357012, + 0.000001920160959137731, + 0.0000019235592760794417, + 0.000001924259777672338, + 0.00000192635554949178, + 0.0000019349791906730835, + 0.000001939895320281056, + 0.0000019408602996000813, + 0.0000019447003660272487, + 0.000001946363326284194, + 0.0000019466650521863703, + 0.000001947723751101471, + 0.000001955772938126748, + 0.000001969457295859199, + 0.0000019994556245310046, + 0.00000202397807225649, + 0.0000020392485596149937, + 0.0000020407397162152945, + 0.0000020465980285149055, + 0.000002050752813970939, + 0.0000020717012472039587, + 0.0000020853881574459375, + 0.00000208719944016795, + 0.0000020945289093743646, + 0.0000020978080051514946, + 0.0000021249061668599497, + 0.0000021351409377186844, + 0.0000021428490137599134, + 0.000002145654819564772, + 0.000002191617879800805, + 0.000002213291195011184, + 0.00000225702899242786, + 0.0000022882335199440166, + 0.000002291646190053892, + 0.0000023522142276147225 + ] + }, + "completed": true, + "percentSlower": 83.37 + }, + { + "name": "Uint8Array", + "ops": 777665, + "margin": 5.5, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 43, + "promise": false, + "details": { + "min": 8.314839084503288e-7, + "max": 0.0000015720711993774453, + "mean": 0.000001285900476510282, + "median": 0.000001364555918001855, + "standardDeviation": 2.3642355744364415e-7, + "marginOfError": 7.066633882608417e-8, + "relativeMarginOfError": 5.495474970027288, + "standardErrorOfMean": 3.605425450310417e-8, + "sampleVariance": 5.5896098514308106e-14, + "sampleResults": [ + 8.314839084503288e-7, + 9.010875947423967e-7, + 9.10794556909399e-7, + 9.181339878687091e-7, + 9.188428475486904e-7, + 9.220826270960589e-7, + 9.229268071679086e-7, + 9.454965514300639e-7, + 9.498610764655089e-7, + 9.741443815493513e-7, + 9.802370931806795e-7, + 9.88516400695044e-7, + 0.000001244395023825514, + 0.0000013009823573933716, + 0.0000013197859861204388, + 0.0000013297967955483067, + 0.0000013312862686153485, + 0.0000013378602556312427, + 0.0000013484920208513225, + 0.0000013528798277314062, + 0.0000013539825812572622, + 0.000001364555918001855, + 0.0000013757224620763911, + 0.000001396532614836846, + 0.0000013982025861609476, + 0.0000014012640101484964, + 0.0000014019876022045263, + 0.0000014099236517530675, + 0.0000014214490389842975, + 0.000001434278923747695, + 0.00000144567595168804, + 0.0000014541757118338716, + 0.0000014615554063129617, + 0.0000014646456554414916, + 0.0000014691683669662178, + 0.0000015099801613952051, + 0.0000015165714392316138, + 0.0000015235163473941177, + 0.0000015361014423230676, + 0.0000015370936497276322, + 0.0000015540870723933182, + 0.0000015620923278646583, + 0.0000015720711993774453 + ] + }, + "completed": true, + "percentSlower": 75.01 + }, + { + "name": "Uint8Array slice", + "ops": 704702, + "margin": 3.28, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 56, + "promise": false, + "details": { + "min": 9.466825519073292e-7, + "max": 0.000001752830375807491, + "mean": 0.000001419039997608682, + "median": 0.0000014308640585951622, + "standardDeviation": 1.7743910771947288e-7, + "marginOfError": 4.6474144367079995e-8, + "relativeMarginOfError": 3.2750411859705606, + "standardErrorOfMean": 2.3711298146469386e-8, + "sampleVariance": 3.1484636948282696e-14, + "sampleResults": [ + 9.466825519073292e-7, + 0.000001002635053361206, + 0.0000010153705420995553, + 0.0000010180521666692864, + 0.0000010188845543278373, + 0.000001057688178803263, + 0.0000012197093975449129, + 0.0000012727659337032206, + 0.0000013199296795184132, + 0.000001334652342706254, + 0.0000013399064332081166, + 0.0000013429299152822094, + 0.0000013437744526350532, + 0.000001355786052213822, + 0.0000013594274554799365, + 0.000001362739449570124, + 0.0000013679910252581613, + 0.0000013688564984361003, + 0.0000013698129607217517, + 0.000001376227024818069, + 0.0000014026022507583734, + 0.0000014038517988777642, + 0.0000014162632067019788, + 0.0000014168258805777785, + 0.0000014171283655281895, + 0.0000014215196862769754, + 0.00000142712005092498, + 0.0000014299315813463685, + 0.000001431796535843956, + 0.0000014413956430850479, + 0.0000014475349166182042, + 0.000001464638872734703, + 0.0000014822793486632192, + 0.00000148792886220392, + 0.000001488276566650425, + 0.0000014960735425868004, + 0.0000014975580529053958, + 0.000001498453436650268, + 0.000001525590195369599, + 0.0000015362571239960392, + 0.0000015378134322493438, + 0.0000015452753092435126, + 0.000001547488266821747, + 0.000001558433145246216, + 0.0000015645146094965656, + 0.0000015653989909309525, + 0.0000015814352828379675, + 0.000001582439966678717, + 0.000001583819106298037, + 0.0000015940263741099917, + 0.0000016174324222372413, + 0.0000016191956525155995, + 0.0000016238800119453657, + 0.000001631607704760857, + 0.0000016338016283419518, + 0.000001752830375807491 + ] + }, + "completed": true, + "percentSlower": 77.36 + } + ], + "fastest": { + "name": "Buffer.allocUnsafe", + "index": 1 + }, + "slowest": { + "name": "Buffer.copyBytesFrom", + "index": 4 + } +} \ No newline at end of file diff --git a/benches/results/buffers/buffer_allocation_metrics.txt b/benches/results/buffers/buffer_allocation_metrics.txt new file mode 100644 index 00000000..f1d41fd8 --- /dev/null +++ b/benches/results/buffers/buffer_allocation_metrics.txt @@ -0,0 +1,26 @@ +# TYPE buffers.buffer_allocation_ops gauge +buffers.buffer_allocation_ops{name="Buffer.alloc"} 737442 +buffers.buffer_allocation_ops{name="Buffer.allocUnsafe"} 3112051 +buffers.buffer_allocation_ops{name="Buffer.allocUnsafeSlow"} 949633 +buffers.buffer_allocation_ops{name="Buffer.from subarray"} 1700664 +buffers.buffer_allocation_ops{name="Buffer.copyBytesFrom"} 517646 +buffers.buffer_allocation_ops{name="Uint8Array"} 777665 +buffers.buffer_allocation_ops{name="Uint8Array slice"} 704702 + +# TYPE buffers.buffer_allocation_margin gauge +buffers.buffer_allocation_margin{name="Buffer.alloc"} 2.08 +buffers.buffer_allocation_margin{name="Buffer.allocUnsafe"} 2.56 +buffers.buffer_allocation_margin{name="Buffer.allocUnsafeSlow"} 3.18 +buffers.buffer_allocation_margin{name="Buffer.from subarray"} 2.07 +buffers.buffer_allocation_margin{name="Buffer.copyBytesFrom"} 1.49 +buffers.buffer_allocation_margin{name="Uint8Array"} 5.5 +buffers.buffer_allocation_margin{name="Uint8Array slice"} 3.28 + +# TYPE buffers.buffer_allocation_samples counter +buffers.buffer_allocation_samples{name="Buffer.alloc"} 78 +buffers.buffer_allocation_samples{name="Buffer.allocUnsafe"} 80 +buffers.buffer_allocation_samples{name="Buffer.allocUnsafeSlow"} 79 +buffers.buffer_allocation_samples{name="Buffer.from subarray"} 86 +buffers.buffer_allocation_samples{name="Buffer.copyBytesFrom"} 85 +buffers.buffer_allocation_samples{name="Uint8Array"} 43 +buffers.buffer_allocation_samples{name="Uint8Array slice"} 56 diff --git a/benches/results/metrics.txt b/benches/results/metrics.txt index b4a173fc..448f4a90 100644 --- a/benches/results/metrics.txt +++ b/benches/results/metrics.txt @@ -1,8 +1,35 @@ -# TYPE stream_1KB_ops gauge -stream_1KB_ops{name="send 1Kib of data"} 157 +# TYPE buffers.buffer_allocation_ops gauge +buffers.buffer_allocation_ops{name="Buffer.alloc"} 737442 +buffers.buffer_allocation_ops{name="Buffer.allocUnsafe"} 3112051 +buffers.buffer_allocation_ops{name="Buffer.allocUnsafeSlow"} 949633 +buffers.buffer_allocation_ops{name="Buffer.from subarray"} 1700664 +buffers.buffer_allocation_ops{name="Buffer.copyBytesFrom"} 517646 +buffers.buffer_allocation_ops{name="Uint8Array"} 777665 +buffers.buffer_allocation_ops{name="Uint8Array slice"} 704702 -# TYPE stream_1KB_margin gauge -stream_1KB_margin{name="send 1Kib of data"} 1.08 +# TYPE buffers.buffer_allocation_margin gauge +buffers.buffer_allocation_margin{name="Buffer.alloc"} 2.08 +buffers.buffer_allocation_margin{name="Buffer.allocUnsafe"} 2.56 +buffers.buffer_allocation_margin{name="Buffer.allocUnsafeSlow"} 3.18 +buffers.buffer_allocation_margin{name="Buffer.from subarray"} 2.07 +buffers.buffer_allocation_margin{name="Buffer.copyBytesFrom"} 1.49 +buffers.buffer_allocation_margin{name="Uint8Array"} 5.5 +buffers.buffer_allocation_margin{name="Uint8Array slice"} 3.28 -# TYPE stream_1KB_samples counter -stream_1KB_samples{name="send 1Kib of data"} 82 +# TYPE buffers.buffer_allocation_samples counter +buffers.buffer_allocation_samples{name="Buffer.alloc"} 78 +buffers.buffer_allocation_samples{name="Buffer.allocUnsafe"} 80 +buffers.buffer_allocation_samples{name="Buffer.allocUnsafeSlow"} 79 +buffers.buffer_allocation_samples{name="Buffer.from subarray"} 86 +buffers.buffer_allocation_samples{name="Buffer.copyBytesFrom"} 85 +buffers.buffer_allocation_samples{name="Uint8Array"} 43 +buffers.buffer_allocation_samples{name="Uint8Array slice"} 56 + +# TYPE streams.stream_1KiB_ops gauge +streams.stream_1KiB_ops{name="send 1Kib of data over QUICStream"} 1540 + +# TYPE streams.stream_1KiB_margin gauge +streams.stream_1KiB_margin{name="send 1Kib of data over QUICStream"} 8.78 + +# TYPE streams.stream_1KiB_samples counter +streams.stream_1KiB_samples{name="send 1Kib of data over QUICStream"} 49 diff --git a/benches/results/stream_1KB.json b/benches/results/stream_1KB.json deleted file mode 100644 index 7fa9bb83..00000000 --- a/benches/results/stream_1KB.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "name": "stream_1KB", - "date": "2023-08-18T06:43:22.348Z", - "version": "0.0.17", - "results": [ - { - "name": "send 1Kib of data", - "ops": 157, - "margin": 1.08, - "options": { - "delay": 0.005, - "initCount": 1, - "minTime": 0.05, - "maxTime": 5, - "minSamples": 5 - }, - "samples": 82, - "promise": true, - "details": { - "min": 0.005767022555555556, - "max": 0.007130931333333334, - "mean": 0.006382768020325203, - "median": 0.006382441388888889, - "standardDeviation": 0.00031921242575240033, - "marginOfError": 0.00006909218602306676, - "relativeMarginOfError": 1.082479980520215, - "standardErrorOfMean": 0.00003525111531789121, - "sampleVariance": 1.0189657275473169e-7, - "sampleResults": [ - 0.005767022555555556, - 0.005792597444444445, - 0.0057999202222222225, - 0.005884176888888888, - 0.005910856111111111, - 0.005947400666666667, - 0.005974421666666667, - 0.005983826, - 0.006002106, - 0.006003759222222222, - 0.006010671111111111, - 0.006047154444444445, - 0.0060527671111111105, - 0.006066597666666667, - 0.006072378666666666, - 0.006073086666666667, - 0.0060989243333333335, - 0.006100501666666667, - 0.006107313444444444, - 0.006113054222222223, - 0.006123377111111111, - 0.006134009333333333, - 0.006135501, - 0.006143615222222222, - 0.0061465942222222215, - 0.006196987111111111, - 0.006204024777777778, - 0.0062322079999999995, - 0.006232642111111112, - 0.006244778222222223, - 0.006248661222222222, - 0.006256433666666667, - 0.006260417888888889, - 0.006267055888888889, - 0.0062697153333333335, - 0.006291894, - 0.006298452888888889, - 0.006305238, - 0.006340534555555556, - 0.006361219222222222, - 0.006373142777777778, - 0.00639174, - 0.006399717333333333, - 0.006403307444444444, - 0.0064049461111111115, - 0.006431452666666667, - 0.0064417354444444444, - 0.006444774444444444, - 0.006451884666666667, - 0.0064600433333333335, - 0.006461426, - 0.0064656639999999994, - 0.006492094888888889, - 0.006524187777777778, - 0.006537884666666666, - 0.006551734111111112, - 0.006556166888888889, - 0.0065649701111111115, - 0.006588004777777778, - 0.006592720333333333, - 0.006614906555555556, - 0.006627802111111111, - 0.0066431143333333335, - 0.006643693111111111, - 0.006657746333333333, - 0.0066654263333333335, - 0.006673224333333333, - 0.006706349222222223, - 0.006709808888888889, - 0.006712497444444444, - 0.006721891777777778, - 0.006724700444444444, - 0.006771452000000001, - 0.0067860306666666665, - 0.006797571222222222, - 0.006859681777777777, - 0.0068622376666666665, - 0.006877356222222223, - 0.006993827111111111, - 0.007042969777777778, - 0.0071242653333333334, - 0.007130931333333334 - ] - }, - "completed": true, - "percentSlower": 0 - } - ], - "fastest": { - "name": "send 1Kib of data", - "index": 0 - }, - "slowest": { - "name": "send 1Kib of data", - "index": 0 - } -} \ No newline at end of file diff --git a/benches/results/stream_1KB_metrics.txt b/benches/results/stream_1KB_metrics.txt deleted file mode 100644 index b4a173fc..00000000 --- a/benches/results/stream_1KB_metrics.txt +++ /dev/null @@ -1,8 +0,0 @@ -# TYPE stream_1KB_ops gauge -stream_1KB_ops{name="send 1Kib of data"} 157 - -# TYPE stream_1KB_margin gauge -stream_1KB_margin{name="send 1Kib of data"} 1.08 - -# TYPE stream_1KB_samples counter -stream_1KB_samples{name="send 1Kib of data"} 82 diff --git a/benches/results/stream_1KB.chart.html b/benches/results/streams/stream_1KiB.chart.html similarity index 86% rename from benches/results/stream_1KB.chart.html rename to benches/results/streams/stream_1KiB.chart.html index 0653c0e7..541d9bb5 100644 --- a/benches/results/stream_1KB.chart.html +++ b/benches/results/streams/stream_1KiB.chart.html @@ -5,7 +5,7 @@ - stream_1KB + streams.stream_1KiB + + +
+ +
+ + + \ No newline at end of file diff --git a/benches/results/streams/stream_1KiB_FFI.json b/benches/results/streams/stream_1KiB_FFI.json new file mode 100644 index 00000000..02f8f7e2 --- /dev/null +++ b/benches/results/streams/stream_1KiB_FFI.json @@ -0,0 +1,122 @@ +{ + "name": "streams.stream_1KiB_FFI", + "date": "2023-09-25T10:29:26.560Z", + "version": "0.0.21", + "results": [ + { + "name": "send 1Kib of data over quiche FFI with no UDP socket", + "ops": 13144, + "margin": 1.98, + "options": { + "delay": 0.005, + "initCount": 1, + "minTime": 0.05, + "maxTime": 5, + "minSamples": 5 + }, + "samples": 78, + "promise": true, + "details": { + "min": 0.00006654742952127659, + "max": 0.00008965672074468085, + "mean": 0.00007608277327446868, + "median": 0.00007531921941489362, + "standardDeviation": 0.00000677464855106611, + "marginOfError": 0.000001503472677879531, + "relativeMarginOfError": 1.9761013080526808, + "standardErrorOfMean": 7.670778968773117e-7, + "sampleVariance": 4.589586299046214e-11, + "sampleResults": [ + 0.00006654742952127659, + 0.00006656480319148936, + 0.00006667586436170212, + 0.00006689842553191489, + 0.00006695263696808511, + 0.00006699523138297873, + 0.00006713117952127659, + 0.00006721275664893617, + 0.00006725846808510639, + 0.0000675845904255319, + 0.00006760462765957446, + 0.00006793610638297873, + 0.00006836769281914894, + 0.00006847750132978723, + 0.00006853758909574468, + 0.00006885010771276596, + 0.00006885756781914894, + 0.0000689712539893617, + 0.00006912263031914893, + 0.00006926422872340425, + 0.00006974161968085106, + 0.00006983019148936171, + 0.00007017399335106383, + 0.00007101938164893617, + 0.00007115116090425532, + 0.00007265115026595744, + 0.00007333435638297872, + 0.00007345226595744681, + 0.00007353490957446809, + 0.00007368952260638298, + 0.00007385760638297872, + 0.00007390199867021277, + 0.00007416287765957446, + 0.00007473107978723404, + 0.00007476373537234043, + 0.00007495492154255319, + 0.00007501819281914893, + 0.00007508605984042554, + 0.00007525057313829787, + 0.00007538786569148936, + 0.0000755013125, + 0.00007615075930851065, + 0.00007655648670212766, + 0.00007661277927927928, + 0.00007666741356382978, + 0.00007679476994680851, + 0.00007697179255319148, + 0.00007734747606382979, + 0.0000773509800531915, + 0.00007750466066066065, + 0.0000776790079787234, + 0.0000783219255319149, + 0.00007928106756756756, + 0.0000808716876876877, + 0.00008100802552552552, + 0.000081355515015015, + 0.00008136785135135135, + 0.00008221165315315316, + 0.00008226896846846847, + 0.00008246318018018018, + 0.00008271803153153154, + 0.00008283569813829788, + 0.00008341238829787234, + 0.00008347656606606606, + 0.00008394050000000001, + 0.00008397560638297872, + 0.00008407515558510637, + 0.00008408882579787233, + 0.00008487334175531915, + 0.00008490117287234043, + 0.00008498446808510639, + 0.00008573470345744681, + 0.00008736418085106382, + 0.00008774746987951807, + 0.0000877749574468085, + 0.00008816003324468086, + 0.00008894502792553192, + 0.00008965672074468085 + ] + }, + "completed": true, + "percentSlower": 0 + } + ], + "fastest": { + "name": "send 1Kib of data over quiche FFI with no UDP socket", + "index": 0 + }, + "slowest": { + "name": "send 1Kib of data over quiche FFI with no UDP socket", + "index": 0 + } +} \ No newline at end of file diff --git a/benches/results/streams/stream_1KiB_FFI_metrics.txt b/benches/results/streams/stream_1KiB_FFI_metrics.txt new file mode 100644 index 00000000..41fd185f --- /dev/null +++ b/benches/results/streams/stream_1KiB_FFI_metrics.txt @@ -0,0 +1,8 @@ +# TYPE streams.stream_1KiB_FFI_ops gauge +streams.stream_1KiB_FFI_ops{name="send 1Kib of data over quiche FFI with no UDP socket"} 13144 + +# TYPE streams.stream_1KiB_FFI_margin gauge +streams.stream_1KiB_FFI_margin{name="send 1Kib of data over quiche FFI with no UDP socket"} 1.98 + +# TYPE streams.stream_1KiB_FFI_samples counter +streams.stream_1KiB_FFI_samples{name="send 1Kib of data over quiche FFI with no UDP socket"} 78 diff --git a/benches/results/streams/stream_1KiB_metrics.txt b/benches/results/streams/stream_1KiB_metrics.txt new file mode 100644 index 00000000..500ac829 --- /dev/null +++ b/benches/results/streams/stream_1KiB_metrics.txt @@ -0,0 +1,8 @@ +# TYPE streams.stream_1KiB_ops gauge +streams.stream_1KiB_ops{name="send 1Kib of data over QUICStream with UDP socket"} 1319 + +# TYPE streams.stream_1KiB_margin gauge +streams.stream_1KiB_margin{name="send 1Kib of data over QUICStream with UDP socket"} 7.96 + +# TYPE streams.stream_1KiB_samples counter +streams.stream_1KiB_samples{name="send 1Kib of data over QUICStream with UDP socket"} 34 diff --git a/benches/results/system.json b/benches/results/system.json index d0a34e75..bfdfc934 100644 --- a/benches/results/system.json +++ b/benches/results/system.json @@ -30,8 +30,8 @@ "osInfo": { "platform": "linux", "distro": "nixos", - "release": "22.11", - "kernel": "6.1.23", + "release": "23.05", + "kernel": "6.1.47", "arch": "x64" }, "system": { diff --git a/benches/stream_1KB.ts b/benches/stream_1KB.ts deleted file mode 100644 index 1ad4dcdb..00000000 --- a/benches/stream_1KB.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type * as events from '../src/events'; -import type { Host } from '../src/types'; -import path from 'path'; -import b from 'benny'; -import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; -import { suiteCommon } from './utils'; -import QUICServer from '../src/QUICServer'; -import * as testsUtils from '../tests/utils'; -import QUICClient from '../src/QUICClient'; - -async function main() { - const logger = new Logger(`Stream1KB Bench`, LogLevel.WARN, [ - new StreamHandler( - formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, - ), - ]); - // Setting up initial state - const data1KiB = Buffer.alloc(1024, 0xf0); - const host = '127.0.0.1' as Host; - const tlsConfig = await testsUtils.generateConfig('RSA'); - - const quicServer = new QUICServer({ - config: { - key: tlsConfig.key, - cert: tlsConfig.cert, - verifyPeer: false, - keepAliveIntervalTime: 1000, - }, - crypto: { - key: await testsUtils.generateKeyHMAC(), - ops: { - sign: testsUtils.signHMAC, - verify: testsUtils.verifyHMAC, - }, - }, - logger, - }); - quicServer.addEventListener( - 'serverConnection', - async (e: events.QUICServerConnectionEvent) => { - const conn = e.detail; - conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - void Promise.allSettled([ - (async () => { - // Consume data - for await (const _ of stream.readable) { - // Do nothing, only consume - } - })(), - (async () => { - // End writable immediately - await stream.writable.close(); - })(), - ]); - }, - ); - }, - ); - await quicServer.start({ - host, - }); - const client = await QUICClient.createQUICClient({ - config: { - verifyPeer: false, - }, - host, - port: quicServer.port, - localHost: host, - crypto: { - ops: { - randomBytes: testsUtils.randomBytes, - }, - }, - logger, - }); - - // Running benchmark - const summary = await b.suite( - path.basename(__filename, path.extname(__filename)), - b.add('send 1Kib of data', async () => { - const stream = await client.connection.streamNew(); - await Promise.all([ - (async () => { - // Consume data - for await (const _ of stream.readable) { - // Do nothing, only consume - } - })(), - (async () => { - // Write data - const writer = stream.writable.getWriter(); - await writer.write(data1KiB); - await writer.close(); - })(), - ]); - }), - ...suiteCommon, - ); - await quicServer.stop({ force: true }); - await client.destroy({ force: true }); - return summary; -} - -if (require.main === module) { - void main(); -} - -export default main; diff --git a/benches/suites/buffers/buffer_allocation.ts b/benches/suites/buffers/buffer_allocation.ts new file mode 100644 index 00000000..34b3cd69 --- /dev/null +++ b/benches/suites/buffers/buffer_allocation.ts @@ -0,0 +1,46 @@ +import b from 'benny'; +import { summaryName, suiteCommon } from '../../utils'; + +async function main() { + const summary = await b.suite( + summaryName(__filename), + b.add('Buffer.alloc', () => { + Buffer.alloc(1350); + }), + b.add('Buffer.allocUnsafe', () => { + Buffer.allocUnsafe(1350); + }), + b.add('Buffer.allocUnsafeSlow', () => { + Buffer.allocUnsafeSlow(1350); + }), + b.add('Buffer.from subarray', () => { + const b = Buffer.allocUnsafe(1350); + return () => { + Buffer.from(b.subarray(0, b.byteLength)); + }; + }), + b.add('Buffer.copyBytesFrom', () => { + const b = Buffer.allocUnsafe(1350); + return () => { + Buffer.copyBytesFrom(b, 0, b.byteLength); + }; + }), + b.add('Uint8Array', () => { + new Uint8Array(1350); + }), + b.add('Uint8Array slice', () => { + const b = new Uint8Array(1350); + return () => { + b.slice(0, b.byteLength); + }; + }), + ...suiteCommon, + ); + return summary; +} + +if (require.main === module) { + void main(); +} + +export default main; diff --git a/benches/suites/streams/stream_1KiB.ts b/benches/suites/streams/stream_1KiB.ts new file mode 100644 index 00000000..96e0ed03 --- /dev/null +++ b/benches/suites/streams/stream_1KiB.ts @@ -0,0 +1,85 @@ +import b from 'benny'; +import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; +import QUICClient from '@/QUICClient'; +import QUICServer from '@/QUICServer'; +import * as events from '@/events'; +import * as utils from '@/utils'; +import { summaryName, suiteCommon } from '../../utils'; +import * as testsUtils from '../../../tests/utils'; + +async function main() { + const logger = new Logger(`stream_1KiB Bench`, LogLevel.DEBUG, [ + new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + ), + ]); + const data1KiB = Buffer.allocUnsafe(1024); + const tlsConfig = await testsUtils.generateTLSConfig('RSA'); + const quicServer = new QUICServer({ + config: { + verifyPeer: false, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, + }, + crypto: { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + }, + }, + logger: logger.getChild('QUICServer'), + }); + quicServer.addEventListener( + events.EventQUICServerConnection.name, + (evt: events.EventQUICServerConnection) => { + const connection = evt.detail; + connection.addEventListener( + events.EventQUICConnectionStream.name, + async (evt: events.EventQUICConnectionStream) => { + const stream = evt.detail; + await stream.writable.abort(); + // Consume until graceful close of readable + for await (const _ of stream.readable) { + // Do nothing, only consume + } + }, + ); + }, + ); + await quicServer.start(); + const quicClient = await QUICClient.createQUICClient({ + host: utils.resolvesZeroIP(quicServer.host), + port: quicServer.port, + config: { + verifyPeer: false, + }, + crypto: { + ops: { + randomBytes: testsUtils.randomBytes, + }, + }, + logger: logger.getChild('QUICClient'), + }); + const clientStream = quicClient.connection.newStream(); + const reader = clientStream.readable.getReader(); + const writer = clientStream.writable.getWriter(); + await reader.cancel(); + const summary = await b.suite( + summaryName(__filename), + b.add('send 1Kib of data over QUICStream with UDP socket', async () => { + await writer.write(data1KiB); + }), + ...suiteCommon, + ); + await writer.close(); + await quicClient?.destroy({ force: false }); + await quicServer?.stop({ force: false }); + return summary; +} + +if (require.main === module) { + void main(); +} + +export default main; diff --git a/benches/suites/streams/stream_1KiB_FFI.ts b/benches/suites/streams/stream_1KiB_FFI.ts new file mode 100644 index 00000000..e4e476e8 --- /dev/null +++ b/benches/suites/streams/stream_1KiB_FFI.ts @@ -0,0 +1,234 @@ +import type { Host, Port } from '@/types'; +import type { Connection } from '@/native'; +import b from 'benny'; +import * as utils from '@/utils'; +import { quiche } from '@/native'; +import { buildQuicheConfig, clientDefault, serverDefault } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import { summaryName, suiteCommon } from '../../utils'; +import * as testsUtils from '../../../tests/utils'; + +async function main() { + const data1KiB = Buffer.allocUnsafe(1024); + const dataBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + + function sendPacket( + connectionSource: Connection, + connectionDestination: Connection, + ): boolean { + const result = connectionSource.send(dataBuffer); + if (result === null) return false; + const [serverSendLength, sendInfo] = result; + connectionDestination.recv(dataBuffer.subarray(0, serverSendLength), { + to: sendInfo.to, + from: sendInfo.from, + }); + return true; + } + + const localHost = '127.0.0.1' as Host; + const clientHost = { + host: localHost, + port: 55555 as Port, + }; + const serverHost = { + host: localHost, + port: 55556, + }; + const crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + + // Setting up connection state + const clientConfig = buildQuicheConfig({ + ...clientDefault, + verifyPeer: false, + }); + + const tlsConfigServer = await testsUtils.generateTLSConfig('RSA'); + const serverConfig = buildQuicheConfig({ + ...serverDefault, + + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, + }); + + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + const clientScid = new QUICConnectionId(scidBuffer); + const clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientConfig, + ); + + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const sendResult = clientConn.send(clientBuffer); + if (sendResult === null) throw Error('unexpected send fail'); + let [clientSendLength] = sendResult; + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + const clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + + // Derives a new SCID by signing the client's generated DCID + // This is only used during the stateless retry + const serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + + // Client will retry the initial packet with the token + const sendResult2 = clientConn.send(clientBuffer); + if (sendResult2 === null) throw Error('Unexpected send fail'); + [clientSendLength] = sendResult2; + + // Server accept + const serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverConfig, + ); + // Server receives the retried initial frame + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + // Client <-initial- server + sendPacket(serverConn, clientConn); + // Client -initial-> server + sendPacket(clientConn, serverConn); + // Client <-handshake- server + sendPacket(serverConn, clientConn); + // Client -handshake-> server + sendPacket(clientConn, serverConn); + // Client <-short- server + sendPacket(serverConn, clientConn); + // Client -short-> server + sendPacket(clientConn, serverConn); + // Both are established + + // Setting up runtimes + + // Resolved when client receives data + let clientWaitRecvProm = utils.promise(); + let serverWaitRecvProm = utils.promise(); + const clientBuf = Buffer.allocUnsafe(1024); + + const clientSend = () => { + let sent = false; + while (true) { + if (sendPacket(clientConn, serverConn)) { + sent = true; + } else { + break; + } + } + if (sent) serverWaitRecvProm.resolveP(); + }; + const clientWrite = (buffer: Buffer) => { + // Write buffer to stream + clientConn.streamSend(0, buffer, false); + // Trigger send + clientSend(); + }; + const clientRuntime = (async () => { + while (true) { + await clientWaitRecvProm.p; + clientWaitRecvProm = utils.promise(); + // Process streams. + for (const streamId of serverConn.readable()) { + while (true) { + // Read and ditch information + if (clientConn.streamRecv(streamId, clientBuf) === null) break; + } + } + // Process sends. + clientSend(); + // Check state change, + if (clientConn.isClosed() || clientConn.isDraining()) break; + } + })(); + + const serverSend = () => { + let sent = false; + while (true) { + if (sendPacket(serverConn, clientConn)) { + sent = true; + } else { + break; + } + } + if (sent) clientWaitRecvProm.resolveP(); + }; + + const serverRuntime = (async () => { + while (true) { + await serverWaitRecvProm.p; + serverWaitRecvProm = utils.promise(); + // Process streams. + for (const streamId of serverConn.readable()) { + while (true) { + // Read and ditch information + if (serverConn.streamRecv(streamId, clientBuf) === null) break; + } + } + // Process sends. + serverSend(); + // Check state change, + if (serverConn.isClosed() || serverConn.isDraining()) break; + } + })(); + + const summary = await b.suite( + summaryName(__filename), + b.add('send 1Kib of data over quiche FFI with no UDP socket', async () => { + clientWrite(data1KiB); + }), + ...suiteCommon, + ); + + clientConn.close(true, 0, Buffer.from([])); + serverConn.close(true, 0, Buffer.from([])); + clientSend(); + serverSend(); + await Promise.all([clientRuntime, serverRuntime]); + + return summary; +} + +if (require.main === module) { + void main(); +} + +export default main; diff --git a/benches/utils.ts b/benches/utils.ts new file mode 100644 index 00000000..b8d7758a --- /dev/null +++ b/benches/utils.ts @@ -0,0 +1,100 @@ +import fs from 'fs'; +import path from 'path'; +import b from 'benny'; +import { codeBlock } from 'common-tags'; +import packageJson from '../package.json'; + +const suitesPath = path.join(__dirname, 'suites'); +const resultsPath = path.join(__dirname, 'results'); + +function summaryName(suitePath: string) { + return path + .relative(suitesPath, suitePath) + .replace(/\.[^.]*$/, '') + .replace(/\//g, '.'); +} + +const suiteCommon = [ + b.cycle(), + b.complete(), + b.save({ + file: (summary) => { + // Replace dots with slashes + const relativePath = summary.name.replace(/\./g, '/'); + // To `results/path/to/suite` + const resultPath = path.join(resultsPath, relativePath); + // This creates directory `results/path/to` + fs.mkdirSync(path.dirname(resultPath), { recursive: true }); + return relativePath; + }, + folder: resultsPath, + version: packageJson.version, + details: true, + }), + b.save({ + file: (summary) => { + // Replace dots with slashes + const relativePath = summary.name.replace(/\./g, '/'); + // To `results/path/to/suite` + const resultPath = path.join(resultsPath, relativePath); + // This creates directory `results/path/to` + fs.mkdirSync(path.dirname(resultPath), { recursive: true }); + return relativePath; + }, + folder: resultsPath, + version: packageJson.version, + format: 'chart.html', + }), + b.complete((summary) => { + // Replace dots with slashes + const relativePath = summary.name.replace(/\./g, '/'); + // To `results/path/to/suite_metrics.txt` + const resultPath = path.join(resultsPath, relativePath) + '_metrics.txt'; + // This creates directory `results/path/to` + fs.mkdirSync(path.dirname(resultPath), { recursive: true }); + fs.writeFileSync( + resultPath, + codeBlock` + # TYPE ${summary.name}_ops gauge + ${summary.results + .map( + (result) => + `${summary.name}_ops{name="${result.name}"} ${result.ops}`, + ) + .join('\n')} + + # TYPE ${summary.name}_margin gauge + ${summary.results + .map( + (result) => + `${summary.name}_margin{name="${result.name}"} ${result.margin}`, + ) + .join('\n')} + + # TYPE ${summary.name}_samples counter + ${summary.results + .map( + (result) => + `${summary.name}_samples{name="${result.name}"} ${result.samples}`, + ) + .join('\n')} + ` + '\n', + ); + // eslint-disable-next-line no-console + console.log('\nSaved to:', path.resolve(resultPath)); + }), +]; + +async function* fsWalk(dir: string): AsyncGenerator { + const dirents = await fs.promises.readdir(dir, { withFileTypes: true }); + for (const dirent of dirents) { + const res = path.resolve(dir, dirent.name); + if (dirent.isDirectory()) { + yield* fsWalk(res); + } else { + yield res; + } + } +} + +export { suitesPath, resultsPath, summaryName, suiteCommon, fsWalk }; diff --git a/benches/utils/index.ts b/benches/utils/index.ts deleted file mode 100644 index 04bca77e..00000000 --- a/benches/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils'; diff --git a/benches/utils/utils.ts b/benches/utils/utils.ts deleted file mode 100644 index 71c4d103..00000000 --- a/benches/utils/utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import b from 'benny'; -import { codeBlock } from 'common-tags'; -import packageJson from '../../package.json'; - -const suiteCommon = [ - b.cycle(), - b.complete(), - b.save({ - file: (summary) => summary.name, - folder: path.join(__dirname, '../results'), - version: packageJson.version, - details: true, - }), - b.save({ - file: (summary) => summary.name, - folder: path.join(__dirname, '../results'), - version: packageJson.version, - format: 'chart.html', - }), - b.complete((summary) => { - const filePath = path.join( - __dirname, - '../results', - summary.name + '_metrics.txt', - ); - fs.writeFileSync( - filePath, - codeBlock` - # TYPE ${summary.name}_ops gauge - ${summary.results - .map( - (result) => - `${summary.name}_ops{name="${result.name}"} ${result.ops}`, - ) - .join('\n')} - - # TYPE ${summary.name}_margin gauge - ${summary.results - .map( - (result) => - `${summary.name}_margin{name="${result.name}"} ${result.margin}`, - ) - .join('\n')} - - # TYPE ${summary.name}_samples counter - ${summary.results - .map( - (result) => - `${summary.name}_samples{name="${result.name}"} ${result.samples}`, - ) - .join('\n')} - ` + '\n', - ); - // eslint-disable-next-line no-console - console.log('\nSaved to:', path.resolve(filePath)); - }), -]; - -export { suiteCommon }; diff --git a/examples/test_example.rs b/examples/test_example.rs new file mode 100644 index 00000000..988c1d1a --- /dev/null +++ b/examples/test_example.rs @@ -0,0 +1,308 @@ +use std::*; +use std::net::*; + +const MAX_DATAGRAM_SIZE: usize = 1350; + +// FIXME: this is a temp test file, need to remove before merging + +/// Generate a stateless retry token. +/// +/// The token includes the static string `"quiche"` followed by the IP address +/// of the client and by the original destination connection ID generated by the +/// client. +/// +/// Note that this function is only an example and doesn't do any cryptographic +/// authenticate of the token. *It should not be used in production system*. +fn mint_token(hdr: &quiche::Header, src: &net::SocketAddr) -> Vec { + let mut token = Vec::new(); + + token.extend_from_slice(b"quiche"); + + let addr = match src.ip() { + std::net::IpAddr::V4(a) => a.octets().to_vec(), + std::net::IpAddr::V6(a) => a.octets().to_vec(), + }; + + token.extend_from_slice(&addr); + token.extend_from_slice(&hdr.dcid); + + token +} + +fn main() { + + const LOOPS: i32 = 200_000; + let mut buf = [0; 65535]; + let mut out = [0; MAX_DATAGRAM_SIZE]; + + // Create the configuration for the QUIC connection. + let mut client_config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap(); + + // *CAUTION*: this should not be set to `false` in production!!! + client_config.verify_peer(false); + + client_config + .set_application_protos(&[ + b"hq-interop", + b"hq-29", + b"hq-28", + b"hq-27", + b"http/0.9", + ]) + .unwrap(); + + client_config.set_max_idle_timeout(5000); + client_config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE); + client_config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE); + client_config.set_initial_max_data(10_000_000); + client_config.set_initial_max_stream_data_bidi_local(1_000_000); + client_config.set_initial_max_stream_data_bidi_remote(1_000_000); + client_config.set_initial_max_streams_bidi(100); + client_config.set_initial_max_streams_uni(100); + client_config.set_disable_active_migration(true); + + // server config + // Create the configuration for the QUIC connections. + let mut server_config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap(); + + server_config + .load_cert_chain_from_pem_file("./examples/cert.crt") + .unwrap(); + server_config + .load_priv_key_from_pem_file("./examples/cert.key") + .unwrap(); + + server_config + .set_application_protos(&[ + b"hq-interop", + b"hq-29", + b"hq-28", + b"hq-27", + b"http/0.9", + ]) + .unwrap(); + + server_config.set_max_idle_timeout(5000); + server_config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE); + server_config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE); + server_config.set_initial_max_data(10_000_000); + server_config.set_initial_max_stream_data_bidi_local(1_000_000); + server_config.set_initial_max_stream_data_bidi_remote(1_000_000); + server_config.set_initial_max_stream_data_uni(1_000_000); + server_config.set_initial_max_streams_bidi(100); + server_config.set_initial_max_streams_uni(100); + server_config.set_disable_active_migration(true); + server_config.enable_early_data(); + + + + // Generate a random source connection ID for the connection. + let scid = [1; quiche::MAX_CONN_ID_LEN]; + let scid = quiche::ConnectionId::from_ref(&scid); + + // Get local address. + let client_addr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 55555); + let server_addr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 55556); + + let forward_info = quiche::RecvInfo { + from: client_addr, + to: server_addr, + }; + let reverse_info = quiche::RecvInfo { + from: server_addr, + to: client_addr, + }; + + // Create a QUIC connection and initiate handshake. + let mut client_conn = + quiche::connect(None, &scid, client_addr, server_addr, &mut client_config) + .unwrap(); + + let (write, send_info) = client_conn.send(&mut out).expect("initial send failed"); + + // Parse the QUIC packet's header. + let hdr = match quiche::Header::from_slice( + &mut out[..write], + quiche::MAX_CONN_ID_LEN, + ) { + Ok(v) => v, + + Err(e) => { + panic!("Parsing packet header failed: {:?}", e); + }, + }; + + + let server_conn_id = [0; quiche::MAX_CONN_ID_LEN]; + + let mut scid: [u8; quiche::MAX_CONN_ID_LEN] = [0; quiche::MAX_CONN_ID_LEN]; + scid.copy_from_slice(&server_conn_id); + let scid = quiche::ConnectionId::from_ref(&scid); + + // Do stateless retry if the client didn't send a token. + let new_token = mint_token(&hdr, &send_info.from); + + let len = quiche::retry( + &hdr.scid, + &hdr.dcid, + &scid, + &new_token, + hdr.version, + &mut out, + ) + .unwrap(); + + client_conn.recv(&mut out[..len], reverse_info).expect("something"); + let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); + + let mut server_conn = quiche::accept( + &scid, + Some(&hdr.dcid), + server_addr, + client_addr, + &mut server_config, + ) + .unwrap(); + + let _read = match server_conn.recv(&mut out[..write], forward_info) { + Ok(v) => v, + + Err(e) => { + panic!("{} recv failed: {:?}", server_conn.trace_id(), e); + }, + }; + + // Client <-initial- server + let (write, _send_info) = server_conn.send(&mut out).expect("initial send failed"); + let _read = match client_conn.recv(&mut out[..write], reverse_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", client_conn.trace_id(), e); + }, + }; + + // Client -initial-> server + let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); + let _read = match server_conn.recv(&mut out[..write], forward_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", server_conn.trace_id(), e); + }, + }; + + // Client <-handshake- server + let (write, _send_info) = server_conn.send(&mut out).expect("initial send failed"); + let _read = match client_conn.recv(&mut out[..write], reverse_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", client_conn.trace_id(), e); + }, + }; + + // Client -handshake-> server + let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); + let _read = match server_conn.recv(&mut out[..write], forward_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", server_conn.trace_id(), e); + }, + }; + + // Client <-short- server + let (write, _send_info) = server_conn.send(&mut out).expect("initial send failed"); + let _read = match client_conn.recv(&mut out[..write], reverse_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", client_conn.trace_id(), e); + }, + }; + + // Client -short-> server + let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); + let _read = match server_conn.recv(&mut out[..write], forward_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", server_conn.trace_id(), e); + }, + }; + // Both are established + + // main loop + let message = [0; 1024]; + // let before = time::Instant::now(); + // let mut send_time= 0; + // let mut client_send_time= 0; + // let mut server_recv_time= 0; + // let mut stream_time= 0; + // let mut server_send_time= 0; + // let mut client_recv_time= 0; + for _n in 1..LOOPS { + // send data + // let now = time::Instant::now(); + client_conn.stream_send(0, &message, false).expect("Stream send failed"); + // send_time += now.elapsed().as_nanos(); + // sending forward packets + loop { + // let now = time::Instant::now(); + let (write, _send_info) = match client_conn.send(&mut out){ + Ok(v) => v, + Err(quiche::Error::Done) => { + break; + }, + Err(e) => { + panic!("{} recv failed: {:?}", client_conn.trace_id(), e); + }, + }; + // client_send_time += now.elapsed().as_nanos(); + // let now = time::Instant::now(); + let _read = match server_conn.recv(&mut out[..write], forward_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", server_conn.trace_id(), e); + }, + }; + // server_recv_time += now.elapsed().as_nanos(); + }; + // Processing streams + // let now = time::Instant::now(); + for s in server_conn.readable() { + while let Ok(..) = + server_conn.stream_recv(s, &mut buf) + { + // Do nothing + } + } + // stream_time += now.elapsed().as_nanos(); + // Processing reverse packets + 'reverse: loop { + // let now = time::Instant::now(); + let (write, _send_info) = match server_conn.send(&mut out) { + Ok(v) => v, + Err(quiche::Error::Done) => { + break 'reverse; + }, + Err(e) => { + panic!("{} recv failed: {:?}", server_conn.trace_id(), e); + }, + }; + // server_send_time += now.elapsed().as_nanos(); + // let now = time::Instant::now(); + let _read = match client_conn.recv(&mut out[..write], reverse_info) { + Ok(v) => v, + Err(e) => { + panic!("{} recv failed: {:?}", client_conn.trace_id(), e); + }, + }; + // client_recv_time += now.elapsed().as_nanos(); + }; + } + // let total_time = send_time + server_send_time + client_recv_time + stream_time + client_send_time + client_recv_time; + // println!("total time elapsed {}ms", before.elapsed().as_millis()); + // println!("sum times {}ms", total_time / (1000*1000) ); + // println!("send time {}ms {}%", send_time / (1000*1000), send_time * 100 / total_time); + // println!("server send time {}ms {}%", server_send_time / (1000*1000), server_send_time * 100 / total_time); + // println!("client recv time {}ms {}%", client_recv_time / (1000*1000), client_recv_time * 100 / total_time); + // println!("stream proc time {}ms {}%", stream_time / (1000*1000), stream_time * 100 / total_time); + // println!("client send time {}ms {}%", client_send_time / (1000*1000), client_send_time * 100 / total_time); + // println!("server recv time {}ms {}%", server_recv_time / (1000*1000), server_recv_time * 100 / total_time); +} diff --git a/images/quic_connection_negotiation.svg b/images/quic_connection_negotiation.svg new file mode 100644 index 00000000..a2689e03 --- /dev/null +++ b/images/quic_connection_negotiation.svg @@ -0,0 +1,17 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daW9cdTAwMWHdkv6eX2FlvtyRLj1nqTrLle5cdTAwMDfvxjs2XkcjXHUwMDBiY2ywMWDA69X736dcbiemWbpccoSGdt5cdTAwMTApJNDQh+56ajtVT/3n28LC9/Zro/T9X1x1MDAwYt9LL8VCtXLVLDx//ye//lRqtir1XHUwMDFhvaU6/2/VXHUwMDFmm8XOkeV2u9H61//8T6HRXGK6n1xuivX790+WqqX7Uq3domP/l/6/sPCfzt+hc1UrtVLn2M6roTNp6H91t17rnFVcdTAwMWGlrJZWdI+otFboVO3SXHUwMDE1vX1dqLZK3Xf4pe9KZ9XJ+b47eimWjjfqz8WMfN3rnva6Uq1cdTAwMWW2X6vvv6lQLD82Q4tqtZv1u9JJ5apd/nlcdEKvf3yuVadf3/1Us/54U66VWvzbxcer9UahWGm/8q9cdTAwMTDdV1x1MDAwYrWbznd0X3mh/1x1MDAxOaVdgKBcdTAwMTVIaaxT3Z/Ln89cdTAwMDBcdTAwMDRSS3DaeU1HSd+3sOV6td7khf2XLPGf7tIuXHUwMDBixbtcdTAwMWJaX+3q45h2s1BrNVxuTbpZ3eOef/zk7rLKpcpNuc1XQYBcdTAwMGaUQe2MXHUwMDAx49F1T17q3Fx1MDAwNVx1MDAwZkbQylxcd9F8xkb2qiNcdTAwMGL/1730zcJ9KcufqD1Wq+GrV7v6cfV63rjkN1ZDYtX9qsfGVeFdXHUwMDA0pHFOeG2ctFZ+vE+idtf/ddV68W6I1LTahWZ7qVK7qtRu+j9Sql1FvFMttNrL9fv7SpuWsV+v1Nr9R3S+d7HZrD+XS4WBXHUwMDFmTd9cdTAwMWP5XoO/rosjfnT/tdC9RZ3/fPz7//75+dFcdTAwMDO3svvpb+Hnv/45XHUwMDFjwe3SS3tcdTAwMTiCnYlcdTAwMDawXHUwMDE3VmlcdTAwMTTdIz5cdTAwMDNwfnUv2zCFzYPnl92th+1Kbbt9vjxcdTAwMTmAZd/ro1x1MDAwMLj7mbFcdTAwMDAsXHUwMDFkXHUwMDA0wlx1MDAxYWPBWSFcXFd1dFx1MDAwMExfXHUwMDEwgLFcdTAwMWWtM1x1MDAxYay30Ley6SHYYmBBaCmMcVx1MDAxZVB3f89cdTAwMDeiNfZDWDq00tO6u+CeKYa9dkIpuojOToLh63qtfVh549uiXFzPq2uF+0r1tee2doSYruJytdJzXHUwMDAx+fXFauWm1rFVpeteSW9XyOR9vN2uN7rvXHUwMDE26TxcdTAwMDWybc3B61JvVm4qtUI1XHUwMDFmcU42iVx1MDAxYj9vi1xmXHUwMDE0hm57q9QxmPSTYFwiUIbUYVx1MDAxZihcdTAwMTUpTOW0XGZJ6Weg9Lul5baryrOlai53cnC28lxc37hIPSittYGx2jmNikxnV1x1MDAwN3VA6U2gQXtccqDQJ4pJR2dyZCNccq1cdTAwMDO1XHUwMDBmKcM4TNJdXCJEavU3wuRhqUmyOltM9p9zXHUwMDFhmIxydbVS/a92QUlKW5Isju7qPq5WLuTh2rXMQP1x/6p2eXNXvUm9q+uUXG5cdTAwMTRYNjhcdTAwMTawXHUwMDBmk1x1MDAxYVx1MDAwMnBcdTAwMGWcIZ9cdTAwMWaVXHSJ3kw8XW1cdTAwMDIhjHJcdTAwMDKlXHUwMDEyRlx1MDAwZWBcdTAwMTJcdTAwMWRcIlxu1/3sPDxd8M6HpfWPpzt49OCtnJKrK4Xtf/XD11x1MDAwNSudVUaN7us2rtTe0aZYPm/urC7tbKytojhupd6sKkdcYiZ7KjQ41KZ7QfjzSmLgrKOoXHUwMDFk6G1jRKJm1aL35HFrXHUwMDAxpCu6cO1Celx1MDAwMMJcdTAwMTQmKkWB9pwwPEWrKnpejbGq2d1sPru4PVuzOnDSkeyqtJOhUsagUlx1MDAwMkqv5Oh29fWyfG5bq8tP92ur69ncba62ZCa0qzNEJTmLgZdcdTAwMWXJxaQ4TvSmkFx1MDAxNLrAe1RkVrVA42VyhlX5gHxWYbQ34EBcdTAwMGZFZXfpXHUwMDFmzi5ZNeNVyFx1MDAxZkhcYpZcdTAwMWZ24D9cdTAwMWb/6t74rFx1MDAxMXtcdTAwMTe56n4h5/KNLVE9XFzf7EpwjyRcdTAwMTbYpn3/eOevn0YlYdBL0/NqXGboW8WwfM1cdTAwMDLxvWdcdTAwMWNccu56Mrhb0f/qXHUwMDA33Fx1MDAwNdlcdTAwMWRPYdPocG+o52z59rRcdTAwMDRyP7dysrq/X1mvnX1cdLiTcbXWefDhXHUwMDE0elx1MDAwN+5eXHUwMDA3XHUwMDA0KVx1MDAwYsCJKfSub11TRTudRVx1MDAxONBaXG4kK9w9VVx1MDAxY9w9akfekkjcXG7HwX3xLHeyn33aKVb2y3Z99ejVXHUwMDFktHemXG53XHUwMDE3hkOicL+aOdyvZlx1MDAwNndcdTAwMTVcbsT64O5cdTAwMWNndrxcdTAwMTnd5X543L9vZXZPy1x1MDAxNzs5t320fFxcyfuN1KPdOFx1MDAwMrQlj1x1MDAxYZx0XGKhy8tfXHUwMDAwXGKBlFxiXHUwMDFjm1x1MDAxYUP+TmJwN4bcXGJP+sZcbsNw71x1MDAwNqIxLjdvXHUwMDBlSe1hTlx1MDAxYkTTg+PoLvfBav7gbLZ47DvlNNztZqnYflx1MDAxN8ghqHQ60lx1MDAwNjuyR05aN7pccl68OjrTuJzLrjRPj1x1MDAwZuTNxo3bvUs9KinWXHUwMDBmXHUwMDA0KEvBXHUwMDA1QjhcdTAwMDbpfN6rgN6RpJ28QVx1MDAwN8nZYKnJXHUwMDE10EIqQ2ZVqiFcdTAwMGW31LyDbLzXIJxcdTAwMTJ00IBFts5RYO/VXFwtsjyxStTX9p+b5eJS63Hn9e7mqjBcdTAwMDWL3MmcgdLSYFx1MDAxOCCfqIBYbLyff5i1it5cclXKXHUwMDAzXftQ7uEzXFzEX5C04lx1MDAwMlx1MDAwNIk+WGk1uXhcdTAwMThK/3d8U9BcdTAwMDFcdTAwMTlcdTAwMGZleWPGeNu/runhglwif/KCjdaa/WCFw/ZdnFxu0ClvvSRcdTAwMWIrtLf9wNCGVZlOPC6dpuCGrmhPyreLuJ/lOtlcdTAwMTG0b8faXHUwMDE1XHUwMDFmeZVcdTAwMTlcdTAwMTFoVmZcdTAwMDaMkGiMd96EXHUwMDBluyk0Olx1MDAwMlx1MDAxMlxiy7UsdIdcdFxmSthcdTAwMWZH/PWxrp50c9Sqrq5uavpQ5lxuheb2XnYl135BcVx1MDAxMrEqq4DuXHUwMDFmKkFcdTAwMGakeGhwUVx1MDAxMFxi5Vx1MDAxNS2Ks8LKXHUwMDFhPbCoKWe6+zXEVJPd0aLNj1x1MDAwMaHuft238PPYcbeJ3L7i4M9pq8aIu1x1MDAxNVx1MDAxZVx1MDAxNbaOnvNv+7p62s63bvfPq49p123opFxmjELQSOJG+q3X5jtcdTAwMWJIdJ7cY280XHUwMDA1uKZvYVM0+lx1MDAwZVx1MDAwMq4k8aSbXGJmQ83+oCsuXHJcdTAwMDCSzNjujfwt82yz8/SLzddGu1x1MDAxZdRcdTAwMWKtgG7XVf3+XHUwMDFm/z1bvz92XHUwMDAxSSbdSZdGalx1MDAwMyXJz3R6XGZtsKVy7uV5cfXgvmireff0snF9kfpcYlx1MDAwMMlfXHUwMDBlLPs7gMo63Vxyh/nzWlnSw1x1MDAxYTjdjqyLk9NcdTAwMDZIZ6JIQ5E6cEiWeURtQFx1MDAxMVx1MDAwMygymomXmMRpg+TTcFx1MDAxM22XT6Rcclx1MDAxZVx1MDAxZSvFcmm2XG6g/5zTwHxkdEPSXHUwMDFjXHUwMDA1emTzjyGP8PONtlg7kFLMXHUwMDFiITxcdTAwMDUw9GO9Jue8N1x1MDAxM6e40Fx1MDAxMyjEXHUwMDAzUlxuXHUwMDE0bmNyqThSr4ExJONcdTAwMDZcdPReddMxXHUwMDFmiDcyXHUwMDAwR664805LL3pcdTAwMTKDPzRcdTAwMDB6cpyVm9d2eDe+MWH5nXJ8XHUwMDEz72n2Rlx1MDAxMsY6Ml2sPLyzvlvc0I0kVODRgTe0cvuzrGTM0CZ+hzm0IFx1MDAxMUhcdTAwMGWVSdpcdTAwMThbZGVcdTAwMDbX41x1MDAwMklWiC6g6Oj9gVx1MDAwNU07rGk3K305walGNpFizY9Bge5+3bfw89h6XHJ9ZDpTXHRwnFx1MDAxY5OjK7Z4k5ZSxYbecyCtPFhnrFx05Xc7zlxmp1x1MDAxMJVgZKB3SiTny2ghXHUwMDAzTSG6IP+Jy3NDedVuuaxcdTAwMGW4NFZxuVx1MDAxNWlbP5jPRFxuwFx1MDAxNOddf2fFXHUwMDE27zT3KjbBxSHG8ZqEpnhxSI5EXHUwMDA1XGYtj9rI95ByMu1cdTAwMTa/od6j3bQn71xmKU62XHUwMDE2JJnUYepccsl3JudaWCMponZcdTAwMDNr+mJcbi5avPmRXHUwMDE5kOwxNVxcZO5cdTAwMDYjN1Glt1x1MDAxZcEgjK7g4nNzKVVwpNJE4Fx1MDAxY3dcdTAwMTTStVe213XTJP5cdTAwMTS2es7teLr+NsHcjedmITJvpEedXHUwMDEwdljNxICrZoDLXHUwMDFj5e+5QTOPWG15J7gptf/BdVxm/73w7393TjTjdqDYXHUwMDE1JL6DK2Vk462SWlwi6dwxtqpWmtfV/IF5rd9dPpqHwqO+PHnJpl4lXHUwMDE40Fx1MDAwMUVqXHUwMDEySFx1MDAxZEvZ145cdTAwMDBaUTyA1lwiNyRwK2NyKlx1MDAwMXxcdTAwMDBcdTAwMTKFXHUwMDEwkmBOWFx1MDAxOFx1MDAxMs5xdlx1MDAxOY1GYa005P5cdTAwMGVpXHUwMDE5clZIYZOvtIjVXHUwMDEx9sA2ynenazunN0u3i4XD86LGqW7ijuNSTWYsRfRcdTAwMWUu8iY5htqgP1x1MDAwM8ZO5vBl/3G13Dq7LyytXHUwMDFmLW3u5JVJPzC4Pc5TlFx1MDAwNVxcv9BXTlxmnrxcdTAwMTRryYCBR7QqwSzHRNXEXG7Jkmuj5oqDTDGzc772kC2fiHq+dVpfluaxmSpb+aea+N0zjlx1MDAwNrtcdTAwMDTW+uO0r8vHwvX+5rltXFxcdTAwMWaVdkw1u7wp71x1MDAxNr9cdTAwMDbapekkvdiA9LrGSHZQXHUwMDBiLS35rChBJedcdTAwMTlPWk7sNTqJcq67mi9bd4+13YPnXFxr2W/dbVx1MDAxNrdcXPPAfFG8/87lxFJF4r1TUsF7YiPDvX63fX93dZFbeytbv1T0XHUwMDE5XHUwMDA19bfUw91TICy5XFzGUrgr+7YtQWPgXGJ8giwo4cr4JFN9XCKw3lx1MDAxYqc6uZ3ROvjIJbFSXHUwMDFhpeeK9sStu1x1MDAxN2E0zKqGoUUw7lx1MDAwNKT/XFz48epd6XV+XHUwMDE1XHKfLSfRpkJcdTAwMTfJoIFcdTAwMDKMcGhG11x1MDAxM6vPNzf7Ny6Pm1x1MDAwN9U7t673mu4p/aw2XvvAXHUwMDBiUlx1MDAxM45Nv+zb7ETpXHUwMDAz0iFcdTAwMTSIelYjLrlOX21cdTAwMDJDJ0FcbjjQgpIjtVx1MDAxZChAZylUm2tLYeJOwVxm1cTXXGJcdTAwMDImXHUwMDAze4jTYaCWiVxyoTBu9Fx1MDAxOCBzty9cdTAwMGXXs/fle3d3ninuy0xcdTAwMTbKqVx1MDAwN7sxMuD8t1JcdTAwMDQxj73dXGZcdTAwMTTkXHUwMDA3itBHtlx1MDAxYVx1MDAwMFx1MDAxMizaJstcdTAwMWV0XG6qNGqhrFx1MDAwZeWy4lwiXHUwMDAwh0BOXHUwMDA0JF/HXHUwMDEwXHUwMDA39o2j5eXNnc370sbZ88XFplrK60w1VWBcdTAwMWY9XHUwMDAyaNNdrf1r4T9cdTAwMGJsfVx1MDAxN/6aLe4jT55oXFzgo6ubmXbRWCVHz/ptVzaPjlx1MDAwZm9WXe2sdtc6eZNvdjH1O2RsMFx1MDAwM1x1MDAwZiikXHUwMDEzoHS4fKlT3aQg8Fx1MDAxNJk7753hPmvdt7IpcnvIgJwsWobvb1iKbyt2mlx1MDAwZU8+7/dcdTAwMTlOJ6KinFxip9XCZan6r1x1MDAwNbaUcrYgXHUwMDFkfuZEXHUwMDFigW1kxbGTyFVsY7B8XFy0KjvyYqWGXHUwMDE3h8uV3N3zpXJbqW/7R/LGXHUwMDAzR1x1MDAwZTBcdTAwMDXvXGLW9G1hXHUwMDFiy/vKxnm6XHUwMDE4TiSXlSd0cs2Zk+QqgIRhjVVD0KmFRfBm7tQ7s0fn1dzQeTV9dEbW0MVcdTAwMDTMUkhLQaJcdTAwMWaDyDk+wZJSfFx1MDAxYa90ILllkEyWgtBcdTAwMDV579P3gbVGaYuEXHUwMDFj5fpZ96bZXHUwMDFk5Fx1MDAwM+NcdTAwMTRZaVx1MDAwMZxcdTAwMWJcdTAwMWaSWFx1MDAwM1x1MDAxMWjjrfGgabWgXHUwMDA3Olx1MDAxZulFq7VcdTAwMGaTb/+GJXTxXHTchXCxXHUwMDFheHKMXHUwMDA0Wlx1MDAwYl4ytW7ooPdaNdK+5Kt4ID1jyFx1MDAxM1x1MDAxMT9rt8asn4vfMO5ZXHUwMDEy6V7ljUWtjfQ2tJNcdTAwMTVak6Xg1WupnWE+XHUwMDE4P7CmL1Y/l4mUbX5cZkh19+u+hZ/Hb+s2kVx1MDAxOVx1MDAwMifBUIBcdTAwMDCjXHUwMDA3XHUwMDA38Vmh1Oo2LbivlGw45+FM/1x1MDAxZSFcdTAwMDZGXHRPko/oXHUwMDFkJJdcZpTO0ZkoXG6hW8xd5GZIbGBcdTAwMDI0wNWujqCBVlx1MDAwZWZcdTAwMDdRaKVcdDtzYtedjXLbbJ9cdTAwMWTunFbPXHUwMDFipZ2dq9XTs2LpYud2mCZRgUZuIOCqXHRgZpdcdTAwMDE9XCIxUDx6QLHCXHUwMDAx51x1MDAwN+twR9Jt8dvjPbqNQj+jJXjSutpw1c2wNdFCvJRM92lcdTAwMTSYL9/9kIlcdTAwMTZuflxmivWY6i0691x1MDAxMb0n6ims8IIs3sjqzS/vuZxr64e3XHUwMDA3uZ8p1Fx1MDAxN58rud30qzdQgTJcXJhtuPCgl8JcdTAwMWasXG6YUlx1MDAxZslcdTAwMWaSlm5CgrSmLmAsevRKdZo5R1xurnhcdTAwMTOVnJJ59TvMOfWh5pb6ULNcbq5ClmpcdTAwMDCh3K6hw+SZnyE0PlOdWoRqXHUwMDE5XHUwMDE4MppcXFx1MDAxZutUXHUwMDFmnz/BhdxC49kzdJAk3dJcYqW6lo6woI1cdTAwMTNSaVx1MDAxZu6s+OhPXCLtjlImX7I0T/9jTV9cdTAwMWVcdTAwMTZcdTAwMWYrmeuMerjc3Mldb1x1MDAxNd5eh1n7jFximJtWcduRs4ZLuVx1MDAwNs09XHUwMDA1zyhccl1xXHUwMDFl7KBJ40zmgsTvzvUuiuM98nXBXHUwMDFipOV5kINBXHUwMDFm03BYp7V1vFFIduLr+yCRXHUwMDAy3nl7QLan5YO4yFx1MDAwZSXvnHZuXGZcdTAwMGYkXvBSq99Acs21XHUwMDE3gtuLe3NHaMkxdMJZYOhCgvGVtlx1MDAwMSfrjJL4vvUyUlVcdTAwMTbvupD7XHUwMDExrlx1MDAxNvstd2B/1b1cdTAwMTm93OJr1GBOyCMhZWQ6XHUwMDA1lNRcdTAwMWOdj55Oie84SSvajebdVCZBZoPXSySFwDxcdTAwMTJOXG66XHUwMDE4yLzoyaHdi4AsLthO6OOlXHUwMDFm0m1tObVjKFxiV8jMr26QJI9cZlx1MDAwMf3i5Fml5unMxDe4LfSkLqR3IElcdTAwMWYqUILMqVx1MDAxZNbXzPx4ZD/RI91/XHUwMDBmkzkzXHUwMDFix3uvy5XjVvNtqS6e11pVt7fphy+Km7pcdTAwMDU4UuhkYtyQ9LVcdTAwMTRcdTAwMDEzXGJprzyPXHRcdTAwMThG3PeVOPIy0aLNj1x1MDAwMaGemlx1MDAxYlx1MDAxMzOLglDCXHUwMDA0hGNcdTAwMDRqsfc3rapNXHUwMDAwXHUwMDA2XHUwMDAyrGGuRd/XTGJcdTAwMDS9Z7jZX3nP/JDJXHUwMDA1as6N32YtwVx1MDAwMbeb/Z49lF0/pnvZZ9xn/e+F5dl6NXHnT7Kk1EZ2VzP9hmDai5HVgMvdn1x1MDAxZtTe1m5qp/Z458CsPLf3SqlXXHUwMDAzZJdcdTAwMDM0THNBJs6F56l1qlVAXHUwMDA3TCgjhfFokmxcIp2sXFyFTKHgOv+5l6v8KlT/3uUq0T3evv/VXHUwMDBmO21cdTAwMTVY4Fx1MDAxMWYjXHUwMDAzdPms8tKublx1MDAxY5j67mpcdTAwMTmzJ+eYPT5IPUClUFx1MDAwMU9Xd4RccovYm3EwoFx1MDAwMtSC51xyMse9Tm7q+ETFnuSeomdmzTlcdTAwMDVcdTAwMWRzxOdvVez5XHRFSTRrXHUwMDExXHUwMDAyXG701oxuRYu722W109q62sDqZaZ6m7/ffntOPUh5dlx1MDAxYiinjScnllx1MDAxMNJrRa20XHUwMDAx8jhVY8GAx+SaNSWqwDlN/jFcdTAwMDW0yD1fQ2BKq3Hk2Fx1MDAxMCq0tnSHXHUwMDA2iNmUQFqssCZxs1x1MDAxYedfn9mNdaWbhYf23ak+OVBPhYZ9nIJ/PUOOklx1MDAxOC52ozvV0Hp08p6l55XV16W7reMnu3G77bN3Ml9ZST8yPFx1MDAwNCRmwDV53trejiVcdTAwMGIuIKNcdTAwMDVk3lxm8DZGcuZrkjmkXHUwMDA0XHKlnfVf3nr9mUM6UlbbXHUwMDAxOVs93Fx1MDAxOZ9SXHUwMDBiPOCDqFx1MDAxZlx1MDAxZLzao6dcdTAwMTd/cZu7Pt1KP6FcdTAwMTapPi6UsYBcXJWq+5I/lueYoed9LC+V8ylcdTAwMWJDSsqCYr45XHUwMDBmQco2zZI83oRLsd+6wUVXzy6jm2ruR4fB8Ic3aNJcdTAwMDBcdTAwMTJcIlx1MDAxMzxeksQpg6PHj64hS9WVwr56vbk+frltPNZuyyr9YFeCu4K96lx1MDAwNMx93Vx1MDAwZU6RJiA30Fx1MDAxOeQ0sEquWXBC1iCttOVcdTAwMTlGc8301u/FQ1ubwt7D+YXJ7fnll/ur8lx1MDAxN0X719ixnnq6iOJQz9RZo7vbj1x1MDAxYlx1MDAwN+3d0vJh/XivuJgv3ZZcdTAwMTZfK/nUo53MKV1T5zpsmOh7KVx1MDAwMblzmCSOo3KjtElwwPhkXGZcdTAwMDFG8IbTXGaaXHUwMDBm48B+nX2TW9d3x/7lYXn9rHK8U38ovH1RsP++XHUwMDA0XHUwMDAx0UUr0Vx1MDAxNp8uvfVA5m50/z7ez0upXHUwMDEyQG98IFxi5c5Z48OcP+8xN0FTKa5cdTAwMWHUZPMxOSWgNVx1MDAwNnR647SxXHUwMDA2lFx1MDAxYqJcdTAwMDOApy9cdTAwMTkvXHUwMDE0RVx1MDAxYVx1MDAxMsOb7lx1MDAxZjNPgWswXFzyRGLzLFvJqfxGXjfXb17PNi9cdTAwMGJ3R4/Vo+PV4Vx1MDAxNVwiXHUwMDEyXHUwMDA01+BcIlx1MDAwMvCQx6HDRpSUpHKRojuktU842jE+tFxyrYlcdTAwMDdOOo88kcVx4yX3XHUwMDExXHUwMDBmLErKgHw5tFpcdTAwMGJrlfQgXHUwMDA3VvXFSnAjpZtcdTAwMWZcdTAwMDNy3f22b+Hn8XuAojmQkFdC0fTohGfxYpda7aYg4NiNh4dgmHCzo91cdTAwMTRnXHUwMDE0XHUwMDFk31x1MDAxMCW4VS457TZcdN+ZV4K90N89edHV9H/4zr5PXoCrQlx1MDAxY1795SmkZnmQ+OhcdTAwMWJr8UFsasFuXGa3vTJm2Fx1MDAxNvexXHUwMDFiOt7+ljw/VzFcdTAwMGa6Tlx1MDAxMO0gXHUwMDAzi4Y5I6xBI4bsq2FAjlxuXHUwMDAyyT85VuDBXHUwMDBl9jM7w5WmvzdZg7pu3j88e+dkzeNu/aCYP97JRfhccoJiPWmEI19cdTAwMDV5Rm6IJqDbuUNiXHUwMDBltGa67jzObTJnJj5117coycWIdFx1MDAxZqXmXHUwMDBlXyNcdTAwMDdcdTAwMTYluVwiXHUwMDAzrWJcdTAwMTJcdMnykHxcdTAwMTFu0s5MlHjzIzMo2VNyZ5SOrsO172XQY1xmPIqXvNRqOHJnLDfVMSmJ031cdTAwMTlcdTAwMWJNNkVcdTAwMTlSbsyjYWyCwdpkXHUwMDFkReTeSl76XFx5npPPz87OoflcdTAwMWH52Vx0t15tNIOB4NpP7lx1MDAwMlx1MDAxOFx1MDAxOe7nxztbK7n85tlx02Q2XHUwMDBlj1x1MDAxNy/LO6mf4oBeUFxmz50njlx1MDAxMG1lb3+0NS5cdTAwMTCefFx1MDAxOeWMtkakrJyPzKHqbCTNx3+ZXHUwMDFlXHUwMDFj/97VfJNcdTAwMTCM8FBK47RcdTAwMWU93lg/a8P149HW9UHZq2v0ZVVdTj19XHUwMDAxOs9dMc47hVx1MDAxNNab3vmqvH9iOuQ7SjDlSnJ1fFx1MDAxM/GL0KJIh+JcZkYqpVx1MDAxMp+/XHK/SKS3bFwi01x1MDAwMZJjO2/NXHUwMDE41fCuXHUwMDAxLXX/drBYr7ij0uKxqp68XFykXHUwMDFkn4YsY0DOMHdcdTAwMDOGu487tfCS59BKLtkzXHUwMDE0o+hcdTAwMDSLXHQn6lVhkoaZjET5XGadXHUwMDE4ltY/vSo/ROCXraeLtJ5KKJ6c48eoNYpnhUstOimKpLBdXHUwMDBiknPefezBJ6JcYpxcdTAwMDGyUWRajdZcdLJzTeLcWnK+weJ8a96TXHUwMDFmRTJz8P8tXFxnXHUwMDFiSfyltPZyjC7S6lx1MDAxNrZlZel25U3KTPXZ1V7U1XrqgS9QXHUwMDA1zPllufHE94/t9jqwPCZdXHUwMDE5IOTLXHUwMDA0q1xmJZhcdTAwMDCZeZ1cdTAwMDNcdTAwMTa0Ylxi7ddgXHUwMDEyS1x1MDAxYtTcXHUwMDFhMPcm0l/F5lj93q1S+1x1MDAxZq3O1K7lXHUwMDE5z1x1MDAwZYs8eZK5J6Wid85cdTAwMTUo4G6c0XNPXHUwMDE3KzmoQdaVzPL5cmv38ax4X26kXHUwMDFmpV5cdTAwMDRcdTAwMWGtXHUwMDA3g8zt0Jt7kuRZXHUwMDAzmWWlpDKeW2vTZZ+NpXVcdCNcdTAwMTPnrvpjQZO0oNGb3VwiprRF8tx3UKP7z/FVnKlcdTAwMDWogIA8ZIfaopb9jTlcdTAwMWUg8EjBXHUwMDA0c6Vob5NcdTAwMGJwKVxcYVwiOy+kJItccsN2u41cYrxzhFx1MDAwNlorraVnsuDPdFx1MDAxNCtcdTAwMTM3g0n389zuLu038vD8hK5RyqnXJfG2cbbTXHUwMDFhXrpcdTAwMDfKXHUwMDBi7qrV1nqnxZBcIjlcZqy2RimvjVx1MDAwMGEmJM+Mr1xc71lcdTAwMTJcInggXHKMdKmwm9z9WJFcdTAwMGVoKVJoK9FLzyp4YElfbKc7UrT5kVx1MDAxOZTq7vd9XHUwMDBiP4/fmlx1MDAxMENccuxcdMvcIzuydotcdTAwMTe6lGo3dFx1MDAxNFx1MDAwN1B8oHnmXG5cdTAwMDVcdTAwMDG9O90kZ4GVWllSXHUwMDE41vlcdTAwMDRbgTnDj95cdTAwMGLStujQ69Eop5iBw/VcdTAwMTRcXP2WvVx0XHUwMDEzqcqJQpBOe0Aw++3uYedNNPCw0X5cclxiXG6XVdi+fob8xbPrs8KROShcdTAwMTf9yu1y/mqlsVdL/VSSzqY3qVQtXGZcdTAwMDIo21uyS7FGXHUwMDAwvPevwVxuJCOZZNwxft5eSTJ75HXNaVxiyfSg+fdO20fGXHUwMDFk0Vx1MDAxM0VcdTAwMTFsXHUwMDFmedFn6IynLEkpOimu4plBQCbRWnKP+ivQdFx1MDAwMNp5aZBidvKnXHUwMDEzQ6f2XHUwMDEwSNMp+ZOeXHUwMDEzXHUwMDE0g+j0Llx1MDAwMLBcdTAwMWGFJWfCyFDN/c8kXHUwMDAxaO+YXHUwMDFkcp4hXHUwMDA3uVxudqJcdTAwMWTwXHUwMDExQ454iqSQf59cdTAwMTGBlFx1MDAxZvS2XHUwMDE0XHRJXHUwMDE16lL5YOxXlmJNQ2tWgFx1MDAxYSZcZjpuWqbdqJXdbsE1oWhrxZbaPI1YXHUwMDE0onKanG9cclx1MDAwNn2If6hcdTAwMWJcYlFcdTAwMTgsPVDooXjdQ4a0fSWS20jB5seASE8r4LDR3ENee+NxjHRn/M1Nq15zzC9i0CqJXHUwMDE0Y/XlUpRcdLxB4Vxm+SRcdTAwMDJkgpRcXHT3x2a4JZeD1PGcaVx1MDAwZpJcIuCaR7TRRzDbOdHMdz1iVpBkXHUwMDA04lwiWVx1MDAxMazxnJ9cdTAwMWJdXHUwMDExXHUwMDFjbGW3ctmdy4fmsTu6949HN8Xr8/QrXHUwMDAyZ8jIOSnpp6JcdTAwMTa9RX1cdTAwMWWZXHUwMDFhjKJcdTAwMDHpSFx1MDAxZGBIT6SiqE9yQkTpMF/KnOJcdTAwMGZcdTAwMWJcdTAwMTbWP0V9P0TgVytcdTAwMDdCOf5+M03yyr3UY8BTb1xcZddyb3ap1N45ebnflPo1n36KQOFNYCxcbjRGXCKFXFy98UfgXHUwMDFjxSZaXG5cdTAwMGZhXs1pg5NnXHUwMDA30rmB/lwiR8xcciUsXHUwMDE5MlDHW6l4ivRXXHUwMDA35+iWlNBUK1x1MDAxNduzxeXASVx1MDAxM83YdSeIXHLZifTGglx1MDAxYmM6xObh7f75/uvGxnltff/ebFx1MDAxZZqXfPor+ZzmSW7OeMVccn+hiUrvn1x1MDAwN1x1MDAxNVx1MDAxMFS4bEKwYU1cZpXa2qAzfWXkjjRJ0aJzPLXib1x1MDAwNMnma6NdXHUwMDBm6o1WQPJauX79RzfJ/c+FXHUwMDFm796VXmdcXOczwbJcdTAwMTKFdTRcdTAwMWKvZjJeXHUwMDE4h1x1MDAwYvDkaW1RyvPXs7Un82hcdTAwMTbd2mKrnv5cdTAwMDIgryT3lznN1XH99blSOFxiQFx1MDAxOUvuqWTqoFx1MDAwNFx1MDAwYugn8YS9Nlx1MDAxYXRcbsr0XFxYXFz/OMI/JODXyftcInvBycNCL6V3o1vdy5f6pluvuOZJ7eLwYm/1Kbt+2k49PsntXGasXHUwMDE2xlx1MDAxYkFcdTAwMDBUvSkrKbVcdNBpXHUwMDAxVlx1MDAxYi9VgmxcdTAwMTdcdTAwMDa5ipZcdTAwMDeN2Vx1MDAwZTyHXHUwMDE16FxyWF6Kslx1MDAwMayaf6D6q/hcdTAwMWPd8Fx1MDAxNorFUmPGrnD/OVx1MDAxM23YXHUwMDE20T0toElCnVx1MDAxZL2yXHUwMDFkV3fOXHUwMDFmL46edqB9unjzZDeunzf201x1MDAwZklyhD0oS85cdTAwMDGHoP01s2xQnXSi082tXHUwMDEy5MqWYFx1MDAwM8EzXHUwMDAxx/OGmVNCaavnXjU7O1D+KC+/mmdt++DJXHUwMDEz9WxDU4FcdTAwMDZcdTAwMWG3lXfSK1x1MDAxY5349novtyTNyapv+lxcYf/t5Po2s7WXfpxcdTAwMWFcdTAwMTNcYmlJY3FcdTAwMTVtaG/rXHUwMDFkp8i0cIxfMLz7lVx1MDAxY04ncm2Rq0KVwLmj1Ifl9Y9r+0NcdTAwMDKmMiZJxcwyU95cdTAwMDN4XHUwMDFjg80t03xdNXl7fHnaXsTK6tFTfunlKfUgNaBcdTAwMDPGXHUwMDFmVzmD7Js1KLUkU+uBPE+lvVxiXHUwMDBi4zzGJElDa1x1MDAxNbRcdTAwMWVcdTAwMWWajDyDtlx1MDAxZrX8XHUwMDEzeDR00qD98cbQPdp8+6q4V69cdTAwMTauXHUwMDE2b3ZcdTAwMTev2itcdTAwMTflwv3JXHUwMDE09mg7lSxovDDjXHUwMDE47smcTFx1MDAxZJmXUSRcdTAwMDWC7s9cdTAwMTj9k5n8yuHl5d7StsvVdjLHr/uHO34z/cCwnoRNXHUwMDBi4S2Qwe5cdTAwMDdcdTAwMDaKwHDrtPJoXFzYXHRNxZgkXHUwMDFlwlxiKlx1MDAwNZ1Zv2q9/oxJXG7TXHUwMDE5RPuUQJ5ccjLH7siobMJtq3SS2XZcdTAwMWK7XHK4W7rJ1Vx1MDAwZZqF1KPSXG5cdTAwMTlorXSHqFx1MDAwYqHPp9RcdTAwMGVcdLPGXCJcdTAwMTPgOVx1MDAwN4nOTlx1MDAxOX9OkuxcdTAwMTTz2TnbJ1c4au+fn5xeXHUwMDE2i2JPnFXLrcWynmpcctHsXFzWr8E1PCG3UMxcdTAwMTBPknHHlfkjo/218Jo9b1x1MDAxZaxUb8zSYyO/t2RcdTAwMGVyL19cdTAwMDDtyP1wzKpMP9n1klx1MDAwYrHOXHUwMDBiXHUwMDEwmHTIc4eYSa5cdTAwMTJhwklJKDhdO4NRu3FwXzyyh7nVxlI7l3l125etYu6u2vyicP9cdTAwMWFMnJN2PUROSWGqXCKpSfxGhnu8jlx1MDAxZlx1MDAwYu79W46jwL1cdTAwMGKFsbmKXHUwMDE0byc6IYXDPrhrilx1MDAxMClOXHUwMDA1rVxmMDNQgsZd6MBcbsehJFx1MDAxYng9xLhrXHUwMDFmOC5kZn5UcsvDLVx1MDAxYT9cdTAwMGKRNDP/yTlRi71cdTAwMDeLmlx0sydB54htXHUwMDBmu+r5tiRenX562Nh5u1x1MDAxMSuN9afbkG5cdLU1K89cXE7CI1x1MDAxYUPBSTevtlx1MDAxMKLwJnUpXHUwMDE0kK73XsshXHUwMDE03iO1PcQ7tj2LXHUwMDEyXHUwMDAyePhcdJfcolbeXHUwMDBm6XtQgTNGOS5FQOFgSC/GXHUwMDE367bOREo3P1x1MDAwNuW6+33fws/jk71E7iVbOlx1MDAxZM9dXHUwMDFlXb3Fy11avVx1MDAxOVx1MDAwZj5AyYOTuJpcdTAwMDP7XG64tHaBQTKoSLi1XCLB2GUyXnGjPI90mC9cdTAwMTlb0rGLXHUwMDBm3cU/tOLfJ9/+ctFTXHUwMDA0wHgv0Y+x/XXqn67Fw9He5vrB1u1NXHUwMDFi11+ONjLphztPeSVccsvdj9KpvsoubVxyWVx1MDAxNnJcdTAwMTFcYopcdTAwMTQn+Fx1MDAwNFx1MDAxM4hcdTAwMTP1OJBcdTAwMTFcdTAwMTBcdTAwMTbMvKlRf1x1MDAxZJB/7/2v6KGMMdxOXHUwMDFljJE9vF6fciDEXHUwMDA2mamNN5RcdHgwXHUwMDE5gZOC9D7if/JRbeCVI10lNCiVIP2J5LSlskh+qCDPWFx1MDAwZWNBkFx1MDAxOGieP0D+M6lccoqFXHUwMDA25zLydoSz853LSKs3alwiyI5cdTAwMThyNO4vXHUwMDBiVy/tfL62enu6tHGwYY/vt4Z795KW4qBD3cS+i1x1MDAxOVx1MDAxMnJILlCgoEQjSLpyg1x1MDAxM1x1MDAxMEeKOOKTa71hXHUwMDEw06Zp61x1MDAxNPnYRqFcdTAwMTjkd8JcdTAwMDBcZjllXHUwMDE2WPTIazZcdTAwMDNr+mpcdTAwMDFHtHi/vz0g2dNcbjmiq8s5XHUwMDEzLzx3XHUwMDFljqzi4lx1MDAwNS+1Tlxi0z9cdTAwMGKN1knCQN/waVx0XHUwMDFjkXT4XHUwMDExPKBPclbbZKOMhNZcdTAwMTSDzkCjxcVcdTAwMWNJJ1D/xFx1MDAxY1x1MDAwYlOJOcKTUFx1MDAwNvs2XHUwMDFkpzPG2DBZbJyr1afzpdP2xebV4Vx1MDAwNajb1rZMP9wp5tCazDBcblRcdTAwMTZ626olOTp0yY0lf8ZpUsbJNYlNRCcreTiv09rMuVxc3YcuzFx1MDAxZj7ZKcZcdTAwMWNcdTAwMTBtj5lrkVx1MDAxNf3oVUXxZVZpXHI5XGZzNzlhPIBcdTAwMTJcdTAwMGX7XHUwMDAxKnSghNVecLmP9Fx0XHUwMDFhZOdJUTDFXHUwMDE0XG6eWjuM2oncg8BcdTAwMDCrXG5uqVx1MDAwNlx1MDAxOarQ/WmfrSfEzm1+ynvEQS6+SJLbKb6us9e7t7RcdTAwMWVFKlxmybfvcnx041xyXHUwMDBlXHUwMDAwNFx1MDAxZsW7WFxuJt3iWDo8vs01bm6PjH2Rpfr+iinD3fAlWWuYQYx0vlx1MDAwNbpVg2RTUpOqpjupXHUwMDAwgCf++eQnpybJ7JSJlmx+XGbI9LRiXHJcdTAwMTnZliPJpnFcdTAwMTbBjlx1MDAxZWvE39+0Olx1MDAxZtyCjtzc0JlLJ/tqs1C7gIJcZopcdTAwMDG4qjJBcifnxid3kp4+gVx1MDAwNpLvlIuLNJKqXHUwMDFjnp5jMzG7079cdTAwMTeWZ+vexJ0/0VwizZj5ycZLw4NcbkfPq9JHzqtQ1kfrR9cnm8fZi1x1MDAwNt6fpV9cdTAwMTF4XHUwMDFmXHUwMDE4h47iLcKU7HNy0GJAblx1MDAwM1lcdTAwMWJcdIZZXHUwMDEwU1x1MDAxNoU4XHUwMDBmSlqYd+30nygkyc5cdTAwMWaI3v1cdTAwMTAsnM6MUW31sra1dXl96Vx1MDAwZpqr2aebxutWW1x1MDAxZldSj1KFhkJcdTAwMTFcdTAwMDM8N9V62VeOYNhRYlwiJXZUJYrkRsNNp/OH8OIsgraJwzbOgpvTQlnsb585fVa8W1q5rF6u795OwYJ3XCJcdTAwMWRSllxi4yiFyVxmWHTWnGTFOaNGN1/72Vxc9mq1eNaU2VuRb+5tnlx1MDAxZdy69Fx1MDAwM8NzoSHLXHUwMDEy2eswldI7MMh8aUGoMeQz0lFcdJqvSTp/PFx1MDAxMDxcdTAwMDQmT1OaXHUwMDFlV/P37/xRMjpzpnhLTeFcdTAwMTioVG9rh9v5k6o7X3nZNPvZ88c3SP88Qy0hoFx1MDAxZmstj1wiXHTn+juft5xaUaBcdTAwMTF1J7mWss5cdTAwMWaD0pP2Tr5cdTAwMWE4zji9vcHa+fl2tb6p25tZvbez4evTXHUwMDFjVVx1MDAxMp5cdTAwMTDzp/Hnc1x1MDAwZjVcdTAwMTLtXHUwMDEwnUuy5IpcdTAwMTlOeY6M9rf7yvnSysV2xl2flrZOL1x1MDAwZp+vqunneNHSXHUwMDA1XHUwMDFhjOdhQFx1MDAwMK5v35pcXMCAeUm1XHUwMDE2ioCVnFx0nqzxh5ncXHUwMDA0k7bNXHUwMDEz7SsntfL90t3TTfvg8fm0un159Vx1MDAwNtNNJs1cdTAwMGXtX2PbetI2v8iEkeaJaCjHKIx/ejrYaeeqZ6uX1TZe3m81XHUwMDBmW/Yq9WDnUcVcdTAwMWP6XHRrXHUwMDEwsa9Q1pBcdTAwMWKsNZl06Vx1MDAxMSUkOKl40lwilc6hYq5zXHUwMDAx0m/a/141KpFb4CaSx5ikXHUwMDBloafW6VPLXHUwMDFle9PTulx1MDAwMy6FXGa8JCfGcy9fX27YXG5cdTAwMTWAQVxue1x1MDAwNbnXXHUwMDE4oiibfo+fYN5V2Wk9XG7xXHUwMDAwdVNO5IBYxSNcdL1xmpz3ge1vo2iBKpwonP3ut9EoQ9wk09/9jrcoXHUwMDBiPd105PWAVNyOXHJcdTAwMDBeXGbZ/5ZcdTAwMDGpeU5cdTAwMGV0yj3R/+wuXHUwMDFic/87PoJcci0qw3vywinUXHUwMDFkXHUwMDA3TlxiXHUwMDFjXFyTorCyU/KupZdO6C9fcFx1MDAxYiHa/Fx1MDAxOFx1MDAxNOrul31cdTAwMGI/j118XHUwMDE3PVKRUMJb8WNMVKzqclurw9rh+UX1+aJZWlxcf1u5T79cdTAwMTejZeCsZF5cdTAwMTJcdTAwMWWs2OfG0Fx1MDAxYnzpnbeiU9mdsl0vXHUwMDAyI2hcdTAwMDHzprubZVDxO256RVx1MDAwNlx1MDAxOTpcdTAwMWGehE4v1Vx1MDAxOKyxp5uHzdumPb4/ap6d1bL7S5WzN5F+eFwiXHUwMDA2wENcYrj2X/p+eFpuXHUwMDA1soLeUNxGk1x1MDAxY9GdNlx1MDAwMfs+aJ1DXHUwMDBiSlx1MDAwZXE9htSnMKC5iOs3TynoMFx1MDAxYVx1MDAxMo0yvkZcdTAwMDJxwkp4XHUwMDE3uV1cdTAwMDBcdTAwMTKNXHUwMDA3O0ZcdMqjK5eyy0+LT1x1MDAxYq581z6C44vGXHUwMDE34IhcdTAwMTbKXHUwMDA3QlkruYNPq74wwzGvXHUwMDEwuSxcdTAwMDaEtmQhk8spTNZ8q0Bznnfue3i/XG7HP723w5NcdTAwMDCRPH7OaFx1MDAxMFwiXFzE8Fx1MDAxOTzjdXJ6k1x1MDAwMJrbM4XqRIr9KT/reMRcdTAwMTCi4+4v61x1MDAxMiyDV4LurbdG0YlcdTAwMDRcdTAwMDVGepizrOhcdTAwMTBcdTAwMTBcYkBcdTAwMTG3VTjYeCusd51RhvPB62xcdTAwMTJcdTAwMDHxXt9CT+OtJtUrNGgnXbinYyFUXHUwMDA379FYLySFouTYmFx0XHUwMDFib+P3tnrWRJdIMcmx51wieKv8INOP1IHXWlx1MDAwYlR8Ra3wXHUwMDAza/pyeYAo4eZHZkCuu9/3Lfw8PrWAip5u6j234o2+oVx1MDAxMV9QlVb1XHUwMDA20jDTj0bBXHUwMDAz6/uHOlmtXHUwMDAykjOnXHIgXHRigoOOQahcdTAwMDB5VpwmPcp8wIPaTaJcdTAwMGI6PPeEQodcdTAwMTSOXGaoN1x1MDAxNlx1MDAxY4Zp8jtcdTAwMWNx6s1q4ZNUb/E1nFx1MDAwYr0pRaBrXHUwMDAynTBRuNBgg1x1MDAwZlViXHUwMDAzXHSdXHUwMDAzpGKfT0+m3V6UPVlqa1neKsn68v1Bc21vXUYsiZQpuW1kKy17msPY1UgtK/J5OzKpmdwjcfWWZJtPpGTzY1Cmx1Rt0SRm0WlcdTAwMTRBy9FCmNHTnPH3N62RXHUwMDE1j3BXmvxcckuQXGZcdTAwMGZlfY+sXHUwMDFj8DhcdTAwMDFDak9cYvA6wek7XHUwMDE2XHUwMDAzy13MnOb2dN1H263tMKRcdTAwMTg71zxKUlXC88ij9M9Qn2+bz+y6fKKp2NGxo6nGqNrYz20/6uzei9g8Mlx1MDAwZpf5/PFcdO5fp19cdTAwMTE4Q2au08ynzUBC1UtcYoCrKbShoDY8WCRcdTAwMTUpXHUwMDE2S1bZk5s2d6qBiTqX/6RYPsuAxs0vIafb+THqpddeXFzlZe+xWDsvrl822lx1MDAwZquQ2a2mPlx1MDAwNuG2XHUwMDFhJEPMzUzOmF54Klx1MDAwMZJQI9FbybRASZpp7jKiiFx1MDAxYj15YjB0irtcdTAwMWUw0468KYFi3oNlf1x1MDAxZJ4hQovPXGZpvVbLrsxlPzLi1CNcdTAwMDFUwURcdTAwMDCNbjJcdTAwMDKjQfGfkfGZ8+uXt7drudNNi7XXzFx1MDAwM4hcdTAwMTW0qcenlVx1MDAxOEjBXHUwMDFij1x1MDAxNI+F+7re8amBWdFQUfTCNFhJklx1MDAwZrrAadXJs1xuY2BcdTAwMTj54CBASadoinHnzlx1MDAwZTpcdTAwMWZ8zth+Rpx6LHx++1x1MDAxMTR8LzRcdTAwMWGHbbqS338mXHUwMDFlvj9VSs9Lg1LzX9edXHUwMDA3XHUwMDA3XHUwMDFmXHUwMDFkdDOQSnxT/vPXt7/+XHUwMDFmrXRWXHIifQ== + + + + + ClientServerINITIALsciddcidRETRYcrypto.ops.random()quicheCM.get(dcid) == nullsciddcidcrypto.ops.sign(dcid, crypto.key)scidtoken: { dcid }label: scid1label: dcid1label: scid2dcidCM.get(dcid) === Clabel: dcid1label: scid1INITIALsciddcidtoken: { dcid }sciddcidlabel: scid1label: scid2label: dcid1label: scid1CM.set(scid, C)label: scid1token.dcidlabel: dcid1CM.get(dcid) == nulllabel: scid2connectcrypto.ops.verify(token.dcid, crypto.key)label: scid2acceptCM.set(dcid, C)label: scid2INITIALsciddciddcidlabel: scid2dcidlabel: scid1CM.get(dcid) === Clabel: scid1INITIALsciddciddcidlabel: scid1scidlabel: scid2CM.get(dcid) == Clabel: scid2ConnID: scid1ConnID: scid2 \ No newline at end of file diff --git a/images/quic_dataflow.svg b/images/quic_dataflow.svg new file mode 100644 index 00000000..a7437858 --- /dev/null +++ b/images/quic_dataflow.svg @@ -0,0 +1,17 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXNcdTAwMWFLsn4/v8LheVx1MDAxZHpqr6x5075vSLKWXHUwMDFiXHUwMDEzXG6EkFx1MDAwNEKAWISkifPfb2bLhqY3NVx1MDAxOFDjXHUwMDExJ8LHplx1MDAxN6q7Mr8vsyqX//717dv37mur8v3f375XXsqlevWmXep//yd9/1xcaXeqzVx1MDAwNlx1MDAxZVx1MDAxMv6/O81eu+yfed/ttjr//te/Sq2WN7zKKzdcdTAwMWbfr6zUK4+VRreD5/5cdTAwMWb++9u3//p/XHUwMDA2fqtdKXdLjbt6xb/APzT8OVx1MDAwMFx1MDAxNf52v9nwf5qDsNwwzdjgjGpnXHUwMDE1f69bucHDt6V6pzI8Ql997+/Dyc7m6u5cdTAwMWKcXsmli9rzYbt1NvzZ22q9ftx9rb8/WKl832tcdTAwMDdcdTAwMDbV6babXHUwMDBmlbPqTfeefj30/eC6Tlx1MDAxM1/B8Kp2s3d336h0OiPXNFulcrX7St9cdTAwMDWG//5cdTAwMTb+/W34zVx1MDAwYv5LKs8oJyVcdTAwMTdOc8lcZlxmjvrXW8E9LfEtgFUgrdShga006802XHLsXHUwMDFmvEL/XHKHdl0qP9zh+Fx1MDAxYTeDc7rtUqPTKrVxxobn9X89smKesIopXHUwMDBimmsjhr90X6ne3XdTT+lU/FnhQnJcdTAwMWOmXHUwMDFhPlwijaC1deNcdTAwMGLIf4ZT0S49VrboikavXlx1MDAwZr7Nxs3Pt/lLkIaiJH9+8/fwXHUwMDEx6fy1sFxiXHUwMDA2xXBEXHUwMDE0u5WX4XNcdTAwMDfkZrnTYlx1MDAwNfl60r2vbuxvnzyLw71a+fvgvL//XHUwMDE5f9v3i93t61bxQbJVdsOvSiv9ncMqe1x1MDAxZP2VX79fareb/az3Pbs+uig+Ld81Ks1Cb710/cY3XHUwMDFlylO4rz48XHUwMDE2qnHwdrTdq3XPxfpqsfVSynbfn39cdTAwMWJOY691U3pXR26cXHUwMDA0wVCGjVx1MDAxYUpwvdp4XGLPcb1Zflx1MDAxOGrwX4FcdTAwMDFHoGNkvoKo4WRcImpcdTAwMTgpjMCxQGbUiJ/9nKOGMlx1MDAxZbNGMaeVXHUwMDEwjDlcdTAwMTdCXHLrPIRcdTAwMGWGL8MqrpWdXHUwMDFkanDrKe7AcamtXHUwMDEyPPDiXHUwMDA3sFx1MDAxMYNcdTAwMTNOKMaEXHUwMDEw8PtAMXIgglxiacKK7GPtXHUwMDE4wjpcdTAwMWNVs9E9rr7RtFxiNvLteumxWn9cdTAwMWSZVl+K8TVcdTAwMWWdbq1cdTAwMWPT/Vx1MDAwMm+Rji3Vq3ck1d/LOOZKe0Tgu1Wk3MFcdI/Vm5sgj5bxXHUwMDA3S9VGpb2Vhf6a7epdtVGqn6SMXHUwMDA330Flc4D2XmDirkudXG5cdTAwMWT1pStVa1NcdN8xXGJ/O1BdoYxUVkuRWXWf1fKDlI/bxaPnLivvd37Ud1ZcdTAwMWbzrrpcdTAwMDKYpzm+XGJkdM056OFL9m+gNffwNWjhXHUwMDFjgGEgQyObou5cdTAwMWFcdTAwMTPD8TrwKt61VSuD1piGxWL1XHUwMDE1c7N6csvaV1ZcdTAwMWOantx2d9Z2PpvV11x1MDAxYXvnO1x1MDAwZrdvx+tyr1F9KPdcbtu8OYX7rlx1MDAxZj49g7jqi/O7UnmNXHUwMDFmn1a2XHUwMDFlMt43XHUwMDAzq49cdTAwMGKUXHUwMDEzsbpTLlx0XHUwMDFhXGZYyUHp7MhcdTAwMTA/+blHXHUwMDA262nrkK+dXHUwMDAxYSRcdTAwMWZcdTAwMDVcdTAwMDbD1byAXHUwMDAxIVxirFx1MDAwM4O/Ji1nTkVxXCJK6mh00eiF+n2YWCROX2k2XHUwMDFhSHkkxLPh9Vx1MDAwZlgujtfjxjR7btfJZjlDM1x1MDAxNKlOZXfmn59cZuu+3d11Wlx1MDAwMu773ebV/stVO/dcdTAwMWFsrSdASGlcdTAwMTlcIlx1MDAxNlx1MDAxYTSjXHUwMDFhXGZcdTAwMDCeVFx1MDAxY6xcdTAwMDFcdTAwMDNMmVx1MDAxOZrlXHUwMDE5qZ1cdTAwMDNjXHUwMDFhpHFDaF1cdTAwMDRur9ybpnqqXHUwMDFktWu6sF4vPOlccnVY+2zP+kFub7z0XHUwMDFlNleXLlR97bpcIlo7r2Jcbve1J7369u5cdTAwMTIrwNqtXHUwMDEw3fX1i5O9ReN2l7jOJ5hcdTAwMDUpmbEmMzTEz37eoVx1MDAwMVx1MDAxNOo+ur1SOCOM42pcdTAwMTRcdTAwMWJcdTAwMWNy7pywYVwidjdojzicKvP7QPHF7kF2T+e53LA7yFx1MDAxNM9dS2d4cDXnQ1x1MDAxNa6au8s1edxb2y1u3lxcPG+f7Ly+5l2FtTZcdTAwMWU4J5WUtLhmQlx1MDAxYayk8lx1MDAxOCqvtVqSLSxCXHUwMDAzy9lSvWHcMqnNXHUwMDE0zPU5Mn+3Uerf9XXnWlabR6fHxzVZeDn7bO/btlr92oXdkGt8eaN7s3FS37CNKdx3+fytudcorF9cdTAwMWPwyt51Yfl5q6Lri8X8YFx1MDAxMr169GIlMqLJ7lx1MDAxM8RPft5Rw1x1MDAxOE+hmlx0o5Rlwb2Ld9Rw3LNCOsaVdsZcdTAwMDTUceqoIbjHNb51/INbZ7It1YNcdTAwMDJcdTAwMWZH/rdo/7jbrpRcdTAwMWVnRPlcdTAwMWaQX+xCfWg8c6B7XHUwMDFi/nbozFx1MDAxYsO5sSo73d9tXHUwMDE0Oy/9w+LKW22p7DaW7lx1MDAxZl/apdwrrlx1MDAwNlx1MDAwZlxycmVcdTAwMDTnVnNcdTAwMTly5pFTPVx1MDAwMIGetlaoulx1MDAxMFx1MDAxYVfO2F6jXHUwMDFhMyPVXHUwMDE0NtzmyPZHh0frnZPNzu5xsys2XHUwMDFie+snl2UxhzXx1PvumZc3LspcdTAwMGatt3t+vta43N6sVU6mcN+bVrW+tXNydWFcdTAwMWUuzp+23m7Ka3J10dg+0UlcdTAwMTBCKiaYzO7mx09+3kHDWM+hY620UFx1MDAxNr2ikI9gQNLmXHUwMDFmd6iqTFo3wzX8idieXHUwMDFiXHUwMDA1hlmE9y++n1x1MDAxYd9/wH754PvAXG5wmO+lUFx1MDAwMiiwK7Pq3m72b697e/roXHUwMDE2dsRTXdrHx8fcx9TQxjvTQlx1MDAxYYd/XHUwMDEzwoS230BcdTAwMWHPOtRozVxyaq7NOeE7yS1cdTAwMWRdrIV91bsvXHUwMDE292vrzFx1MDAxZcku69+9/NCH8NlcdTAwMGLwRdhEkpfbXHUwMDE3+kAu7cJu2+xcdTAwMWboKdz3ol5/2DlqPcFcdTAwMDM3O5fucK/fWa4uXHUwMDE24TuRuOcn0bNcdTAwMDfLXFz2Tfv4yc87alx1MDAxOOZcdTAwMTnmkO9ccnCO/n1cYjWc8pREv97hIW3d7EBjMr7XXG7H9r+2qD9Ttv+A+3LP9kYhXHUwMDA1XCJ3ZF+WOy/2tWzuXHUwMDE0rzeXTm3Z6c1cdTAwMTWx1c+93irn4aPim2BguFKjeotiyTxLR8lrRlx1MDAwZn+GijtcdTAwMDW2l1ZcdTAwMTkwVi5cdTAwMTbZt1x1MDAwZTuFXHUwMDEzWGm9XHUwMDFlX12p2srtxmXvfndcdTAwMGW74qn3XTayXHUwMDA0XHUwMDE3XHUwMDA3jeLq46bWcHa8XHUwMDBmp3xcbvftdS9Xuoe9/cfN435cdTAwMGLKa2e169eMq1x1MDAwNlx1MDAxOcl+5nH3KWSP5qZcdTAwMTWMO5ud7eNnP++ooZ3HnUNcdTAwMGZfcMNEgLfeUcOCh2jhaD3QqVx1MDAwMK3mg+2NpDhe9vl0P46o5pzuPyC/WdN9lnDlXHUwMDEwsIxcYjRcdTAwMDfwOHda0uaT1DK0XmWVQFx1MDAxYVTo/FxuJVGqZURcdTAwMTLBeGBcdTAwMTV3wlguleAxwSSkXHUwMDE0nFk8RSPhaiZSlGxaijJcdTAwMGV8hDfqXHUwMDA3192UOveVXHUwMDA0/Fx1MDAxOFx1MDAwZTlcdTAwMWU/pkjAXCJCwL9cdTAwMDLzKSBcdTAwMDeCO6iBTM5A8GE4nFpcdTAwMDFOlMuUWVx1MDAxOVbtkWFPUblbzWrYllx1MDAxOP7t2/Bt+/9cdTAwMTj8/T//jD07USbpU4iI4/B2XHUwMDExpq2XOt2V5uNjtYvPeUhjXGY/T6dbaneXq42bauNudPZ+5sZmSc7xcazc8+VcbvlcdTAwMDXNb2FcdTAwMWNOXHUwMDEwTlx1MDAxNFx1MDAxYlIqSVWpRW9b+idp9DhRa1x1MDAxOU51REIqjZuPXHUwMDA3lVx1MDAxZVk8MihcdTAwMDFcdTAwMWM405ZMZo1wYVwig9KeXHUwMDE2+D2jWF8hXHUwMDE5j5FaelVLhEH3ldL7hn67XHUwMDFh8lFw3PFnpKBfekBnOvpJz1wi9DnOlELJXHUwMDE1oc15hD1POcuVcFx1MDAwMl83XHUwMDFi0v0v+LPGc1x1MDAxY0mYXlxyt0ivUfhzXHUwMDEy5U1cdTAwMDNcdTAwMWFcdTAwMDRcbv9cdTAwMTje4lx1MDAwYvxcdTAwMDY3nlx1MDAwNPy4MJRcdTAwMTJhVCS0lK5cdTAwMDJcdTAwMWX+drAu5Vx1MDAxY5po8s9Fv0SRpE9IXHUwMDE4c1x1MDAwNX1cdTAwMDXmSa1cdTAwMTjHcUluXHUwMDEx3MBFYVx1MDAwNjygmaeT0Fx1MDAxYsaTJ4S+1LDL0UEpy4TUXHUwMDFjqcQorkBHxsSN56cuvu/6KW303LAvPaQtXHL7cJieNUag1actpW6HsFx1MDAwZo1cdTAwMGJkXHUwMDE5a2gzxOpcdTAwMDBcclxysM8pXHUwMDBmlEInXGK9XHUwMDExYHGGn/CoXHUwMDFjgNGcgeBCfmHftLDPcpQ0oVVku91nreTFPadcdTAwMWNBQqZMuoVcdTAwMDS/XHUwMDA0maRPIVwijvNAv3FsLNQ2ztH2XHUwMDAzsE5ccrVliDN4jrLolCFcdTAwMDQyUFx1MDAxMvhk4JdcdTAwMWWAXHUwMDE2XHUwMDFhk3agjEWD1DgmeGRMXHUwMDFhTUNcdTAwMDCGMILGKJUwmVx1MDAxYvSlx/dkgT5cdTAwMDbMMoNsM4p8Rlx1MDAxOUQ+eihpJVx1MDAwNPZsXHUwMDA2wCc9WkbhRliJoiRjXHUwMDE2X2idiFbRnVx1MDAxM2gzXHUwMDBmb/FcdTAwMDV8g1x1MDAxYk9cdTAwMDJ8KGBgXHUwMDE5xHq8PLBwXHUwMDEywj1rpXRcdTAwMTAsrvOnwV6SRNInJIu5Qj2y+Vx1MDAxMKqdMpxcdTAwMTnmNFxiSMQ97qRAW1x1MDAwNayeXHUwMDEw99JcdTAwMDNxRkel8ZXhb9Gal1aBhd6AIYqmqpNcdTAwMTZli6RLRVxy0VlcdTAwMDFfepxDKvAp5+FcdTAwMDNxf2nBXHUwMDAwXHUwMDBihbg4I8ndtbTuINFNiDH5uGe0XHUwMDEx6Cwj0itcdTAwMTeXZao5ulx1MDAxZmg1XG5AwjDOfK32TVx0+1x1MDAwNO3/4ayYOIdcdTAwMTfVO1x0/MDPOtQuS+TlYoJfklDSp1x1MDAxMJHHueBfVvdcdTAwMTK9S1x1MDAwND60sNAnV0JcdTAwMTgmXHUwMDAzJ71cdTAwMDONoeVKNNkprFx1MDAxND9R+ciEfelhXHQjI5LGIclq9K4lONBRXHUwMDFm3HlA/qJm6Fx1MDAxNCNmzFxy+NL3fNMtPnwsh3hNXHUwMDAx+8JcdTAwMDXzWd437pBimFx1MDAxMFx1MDAwZbhiWrnA2nl4n4OC/Y2xJmahXHUwMDBmmGeB4VtBopVcdTAwMTRX84V8U3J3UexcdTAwMTFpXFy8u2uT3V2G/lx1MDAxNLMmU9D5QkJfolT6XHUwMDA3w/KYK+Qr/DTqXHUwMDE4OI5cdTAwMDNnXFzGWH7Ww/lcdTAwMDOcRYW2LfrEk2Ff+lx1MDAxZW3I7jNGIbhx6azQ3KnIkLjwJNqFjmjV31x1MDAxN3VRsZ06/mUrwOJcdTAwMTK3/JBcdTAwMDA5jldk8oDewbZcXF57rD1s7zeWVmpvXHUwMDFiR8XjjZWd1cmAZ37xXHUwMDE5aLuiIyQ5R+9dMWZG4zNQiqxHnIXy5lx1MDAxY5hcdTAwMTlcdTAwMTZTXHJcdTAwMTRLXHUwMDFi8IOKXHUwMDA0ZGjKtEDhXqzKasu76+XbvYeLl5vmydbSnTlYPXv6MYdcXOlcYmh9Ulx1MDAxY1x1MDAxNC1cdTAwMWYmKppEvFx1MDAxMlx1MDAwNsZQtPjXmXdFXHUwMDAz7qFcdCg4XHUwMDE3XHUwMDE2zVZcYtdCUPNSNG3RvEe7TVN2ozU6xiONXHSEUlx1MDAwNlx1MDAwNNq2U9C7RVx0hCpcIs+UruuVmVx1MDAwNkN9wFx1MDAxOeFgqKQxzTz+mVwiXHUwMDE5kjRY02KOXHUwMDFhJ5Kxqy5Xdne2T+pM1qqty5IqrkEj91xubMCjXHUwMDAyXGZUgpQrJ0J5XHUwMDBimlla+5d0XHUwMDEwddzNsCxBLFVcdTAwMDbirX/6XHUwMDAwnHaulOGLXHUwMDE147wvxen5zU5VtF6KO83H/Vx1MDAxN3fROshP/Y/ZcyVP1jSB/lx1MDAxOVx1MDAxNbVcdTAwMGYsaH2kavGvM++qXHUwMDA2ynNS0n6dNtqxUCFcdTAwMDH0MualasjKgHwsXGZD/4VcdTAwMDXKgqelXGLhXHUwMDAwKUZcbj69Vvf8yPKsXe3OnCw/oI0wWSaNafZ1PZObdFBcdGshYIxcdTAwMWO/7cNcdTAwMGLWfWlurlixflx1MDAwMO6sfnl9f5d7XHUwMDA1tsLXUGessoyHXHUwMDE12Eiq8MNRqVx1MDAwNENNsTNcZvvP5lZcIlXSbrxSXHUwMDBiVtvr7Gaz3Hrj7mrz4ODxYv+23T14mENVjtxwJbORfe3hUiZcdTAwMDOlmcu0i5P2OvOuaiA9NEpcdTAwMTVQwC26ljGhXHUwMDE5c1K1iVx1MDAxY0sukOaF4tOos7MoZDlcdTAwMTfP8lx1MDAwM97IkWeZUjjLXHTDLdgxWlqpXHUwMDE1vrteu2nXmtpcdTAwMWQ+uMrSaf/5MPc6bJnnhDaCisFTLftRXHUwMDFktsx4qCZOXHUwMDE5XG5CXHUwMDEwYoY1cDK6llpxKVxc0OVfXHUwMDA0unyzduttT5Rum8Ksnp523lx1MDAxYbWjzMuwsy82NVx1MDAwN9dSJlx1MDAwNnyhXHUwMDFmxVx1MDAxOG1fZ7dM419n3lVcclxmlZuiTnnOUd5pSNU4zEvVJnItlbaK8eCu1Fx1MDAxZs+Wc3EtP6CN3LiWnIlEi5dic6WQY7SDWmu+nfzoudJqvbXy3C2t7Tw2LlnuNdg4zyluKI/bT+hcdTAwMWXVYFCUoWYsqpXOhW+JdrnUXHUwMDAyXHUwMDE2zLV8vjgobm6y5drNau2oIZo7dyv3xTnUf8pcdTAwMGZXssTYXHUwMDAwx9BlkWKM7krxbzPvilx1MDAwNmiVXHUwMDAyVXI1Slx05UJWKei5KdpEniVcYoRDXHT/S1Q5XHUwMDE3x/JcdTAwMDPSyJFjaVx1MDAxM1NaXHJYjUQxxtqQPNt5tv2z1ublmz1/LpVO2f3TXu412ICHpi44rjUqjlxurVxyOS49TZGcXHUwMDE0x5lcdTAwMGK/XHUwMDEylYb6RCxYXHLGnutfmdaG2+ghz9XaJ5fF5qHMT03DOXBlsqahcKGl5tRcdTAwMTjdiuJfZ95VXHKUZ1x1MDAwNDX8QVtPXHUwMDA3XHUwMDE2Rt41Teh5adokbqU2TlxiY6fR2WRRqHIuXuVcdTAwMDekkSOvMrn2XHK3aHhcdFx1MDAxOIcrt597l5vrXHUwMDE3x0V+c//j6L5Xuj88zH2hMnIrXHUwMDA1+pVcdTAwMTI9S0WVSUc0WDCJ+o2GrtNgXHUwMDExzsSnu5VUysNcIsEvmF9ZuD6r3S2fXHUwMDFm37V3K8/92vrBQ/nZzKHUYH64UiaGwiq/+sg4XXvj32beNVxy/UrqYyRcdTAwMTRHOlRcdTAwMDJCmqbmpmmT7VgySj2T08joWVx1MDAxNLKcz4ZlOmvkyK90KZ1+0O5yVo9R+V9fde36/n7z+uTKVNqrXHUwMDE34uGIP+dehalph1DSUHdObkL1IFx1MDAwNNpcdTAwMTOeopQga1x1MDAwNVx1MDAxN2zua7BcdTAwMTHHUqKLXHUwMDAyOthvcVx1MDAxMbhy/Vx1MDAwN99t9Vx1MDAwZk5ftlx1MDAwZl/bsNO42V4vX/5cdTAwMGaVz6V0iERN445cdTAwMDOM1Vx1MDAxZSf+deZd00B6zlKiNmNWXHUwMDE4XHUwMDFkUjRu5qVok/iVIFx1MDAxY45cdFx1MDAwMsP646lyLn7lXHUwMDA3nDFcdTAwMGa/MktuWlp+uWXcU1xulFx1MDAxM1I6bmRowURcdTAwMDF4QlDpXHUwMDFhoyWa+tHCXHUwMDFhhvtp95pcdTAwMTL9hVxybsZcZlx1MDAwNZIq0lx1MDAwYtpcdTAwMTKVzFx1MDAxOVx1MDAxZFxc3/jKL4+q2Dj55Vxm9Uqy4KpeIPw5JSZcdTAwMTPtfsMhi3W0kNnliUJJn0JUXHUwMDFlh/eLcO7U8svHqF3GKIWbXHUwMDE00jDrJ8hcdTAwMDfOes/lVp6wSEWcOiGjbzaoXGI3Zn55etrbyJhcdTAwMTSSXHJcYm7xt0Rw5fTbMONdofNmteFCW1x1MDAxMsyo0Iazy0de4yR1NdLzjFJxT4NcdTAwMDckXHUwMDAwQmopTahcdTAwMWU+14p5IJRfiFRrXHUwMDFiU1BIXHUwMDFiXHUwMDBmXHUwMDFkY2DUZ1M75oa27rD7XHUwMDA1oH/N0TyXilx1MDAxYlp7/qqrMSXcQ3dcdTAwMDenTYvYgkJcInlcdTAwMTPESWGCdXf/MNgrJFxupX80Ko/zwL30bKFcdTAwMDDGXHUwMDE0XHUwMDEwlq2Pelx1MDAwModvnFx1MDAxOEY6fFx1MDAwYlx1MDAxNFRjmmqAgVx1MDAwMMRcdTAwMTlcdTAwMDT3yYAvM1x1MDAxOOOgXHUwMDE41WhC6IP3tD9cdTAwMTatrKE99Fx0OHNSXHUwMDAyelx1MDAwNzZLYY3fhr70tJF0k4/CmGl3V5I3XHUwMDFlblx1MDAxN2hAeVx1MDAxMidcdTAwMDCoWqdmeijWY0CfUJ5lVNHPgKCgq686ktOy+DgqL2WYxEGfTXTTnV+HS2SKKl5I6EtHvog4zlx1MDAwM/gyXHUwMDE3bWTkYnFcdTAwMGXaoS2Hk2tjijZaT0vgaOkpsNY5PSHupWejjIzJOE6D8rs0cGBRLHZcdTAwMWX6XHUwMDFkaHyiV6ipXHUwMDAzS4Y6kr+NeunR/6moZ9CGXHUwMDE1Vlx0Z1xmN8yEXHUwMDFjXSucx7iVtC9kJUTLqGntWcesQ1uRKrjLuFxukoJTj2SBP8X9LC85XFyP/YK9wY0n65kglLOgolx1MDAxOb3+1CRcdTAwMWF8gGJpqdbVn4p7hWS59Fx1MDAwZkdFclx1MDAxZdCXXHUwMDFlxj9qXlx1MDAxOUq4ouruTklccjKmSVx1MDAwMSquo1x1MDAxYd1cbs0wh5pcdTAwMWJtUjD1XCK6XHUwMDFjJHqHjNJW0U5cbvRPXHTYfE4hWON7lVpcdTAwMGLLVIbWXHS/XHJ/6Vx1MDAwMd1p8Gec80ArXHLGXHUwMDExaNtwtD7VYXZcdTAwMGVcdTAwMWaDkm6Cq/mDdT7hOYPAaDnYmE6PXHUwMDE0WIVcdTAwMTeT/Y5cdTAwMWVcdTAwMTbawEPV+IK+wY0ngT5AV0hcdTAwMDOLNj7FY8l5ZDTJpDN/bFx1MDAwMclYeaRPISqK84C8McrUoo3q0P9GlURvXFxCwFRcdTAwMWRU6EYjXHUwMDE1X7e2imskvVx0q0emR4iPXHUwMDFhe1xubWNqn4tuXHUwMDAyolm0W4L1cKhcdTAwMDJ9b05Jp2gpzVx1MDAwM+7SY3LTrT3pXHUwMDE56lx1MDAxMU15WCzY/+o9XHUwMDBlVEmq8qbwqTUtXHUwMDEyR7tcdTAwMDN+XHUwMDA0d7RcdTAwMGJHdTWAXG6og+ZfxcKnXHUwMDA0d9TUhypcdTAwMTHEXHUwMDE5eipxZc9cdTAwMDJQ9Uz2x+5oXHUwMDE0UvAuXCKL88C79JjaUWtcblx1MDAxZCqgNpz2Pb452lx1MDAxN1B5Tlx1MDAxYupVJ7jicsKmgJlcdTAwMTGYXG74Ulx1MDAwZlx1MDAxNU49tLmkMsMxzq1cdTAwMTSaPHHFrb+2N1x1MDAwN7hLXHUwMDBmq5zcusNHXHUwMDE1XHUwMDFlZZlcdTAwMTguXHUwMDFkKFx1MDAxNtNcZjW0YWZkTFMsITzq01x1MDAwMcrv21x1MDAxYkDFL8RcdTAwMWLceFwi31x1MDAxNvVcdTAwMTb9myB4XHUwMDA1LLzk3ClNbMzYXHUwMDFm69omyyR9XG5hcZxcdTAwMDfoZa7HjVxiwyXt0JJjaFx1MDAwNIW9xUCMo1x1MDAxMoK0gytcdTAwMTC9XHUwMDA3u9DjLumlxmuOjEkjg/idJKi7rDRRXHUwMDFjXHUwMDA2j1x1MDAwZZNcdTAwMTCg6cSjXHUwMDAzmj7mpYfHjbOgXHUwMDE3wjx8Qs/ScqpQ+EwyZlx1MDAwM1x1MDAxNzxHZoMhj1eiQVx1MDAxYreg92XmzVxu9FBxXHUwMDA1vltcdTAwMThcdTAwMGb0OPVcdTAwMTJR1DDgT0W9QrJcXPqHP8faS490XHUwMDBiWXtcdTAwMTZ5SVO9QXQpdXykXGKBXGa66IyiReSE63njNEfwQ9hcdTAwMTRHXHUwMDEzSIJSMfZcdTAwMWXnXHUwMDFl+lx1MDAwZsZIaVx1MDAxNW0+86hcdTAwMTE6ffDTh8dCNVx1MDAwZd6Otnu17rlYXy22XkqZwK/AhfL8kDxGXHUwMDExuGifhlx1MDAwYug47Wl6/0DbSspETT6KJFWK2k1Kw5Bj+fCUXHUwMDAx/kmP+Fx1MDAxN1mDQmHQdrRfjVCnhH/aXHSDdo2MXHUwMDBi3EtrXGYjyYNiJtBHMDv+TVx1MDAxOP6+a+/X7NLZ9tt9f6lcYkfbbPXxtDfbMPXZQGuyxNOnXHUwMDEwXHUwMDE19mlcImtcdTAwMTgspth+mnlMUm9V5FNO3eCti1p2iG+gNdpMTCvqxOainVxiZ9X8Kl5+XCIoN5p94IOcllx1MDAxZbWEpOw0RDKnQr1cdTAwMWEsurVo+1NtW6B4pmi/U7RcdTAwMDFcdTAwMTWF5KBcYnJqXHJcdTAwMTi7lJdcdTAwMTb6/4VpozL9q6Gfn0EvYruZ6ohFN2hgT1tlKltcdTAwMTXJedluv/JcdTAwMDVOV4PVKYdcdTAwMTlcdTAwMDfcjHybkHEweUZBMMJ/SMDhPIN0OyGcZzDyLJHkglx1MDAxNE21q3e3O1xcXHUwMDFm3Vx1MDAxZlT6XHUwMDA3p+p4/WH75Cyqqf54RzSVKsEzJ5SlNnWKhcouoPGlPcVcdTAwMWR+XHUwMDE4uryCR41cdTAwMTEkNVqhslYoQ+fE6OnCqGlK7lFcdTAwMWUsXHUwMDBmp2hcdTAwMDFAxaku18mOl1RolvzBvThcdTAwMTMlMHT1LC2B6FVZ3VxyOpai2OXa6Xlrr99aKV9V6ufnl/asXZVZKFjjW5HUJJ1pkDyQXHUwMDAz7uu1YOBcdTAwMTlgSlx1MDAxYY5mxZCdXHUwMDA3Ws2Vh9yrNLiFJ9/P0Oo47qVcdTAwMGVxhlx1MDAwMeNx8Vx1MDAwMDythrNcdTAwMWbCbHOlwb/Y96hXLd9Xvr0nxn07RuH+tty7vVxyMunknFxcr9x2U1x1MDAxOLnbbI1HxyNcdTAwMGZcdTAwMTbm3lxmT5KNkePJNugkyqSpXHUwMDA2lFx1MDAxMebYXHUwMDE4pZ70xvL108r6zc3r2X532z4+nFRcdTAwMGUmrF8xXHUwMDBiXHUwMDA1ik/J1Vx1MDAxYzxrpEWfWCN6mzA4KeVp5feJUkqzXHUwMDE51q9cdTAwMTCSeY4pXHUwMDA2tGVI/XBiciBcdTAwMDOo9pOGmb/4XHUwMDE1XGLrmSn5zydVdzKmy8ig43DhjGyExIlcdTAwMGVd/lfw/2NrNkRss2FcdTAwMTEoRl6vVllcXKh31T7dqTydXHUwMDE3a2+sdso3e6Xj7tL5ZSfvqi2oXCKMoea/mlx1MDAxY1xuXHUwMDE3anqNPOZRK2zUfiRDNbsqbjhcdTAwMDCPaFNLxFx1MDAxOeN0zFx1MDAxMmlEsalcZlx1MDAwN1x1MDAxN1LOpjfql2bPSLNcdTAwMTMmOnR1JsVOrKORTNlU9V+BZplyXHUwMDEz31x1MDAxNbtV2Nl7OOSl9T35cvRQXHUwMDE0j1x1MDAxYv2NSv5cdTAwMTXbeVx1MDAxNFMtKV5dXCJcdTAwMDOGXG5pXGKmPb9cclxypdKBXGbE7U1ftanOnEF1XHUwMDEw1l9cdTAwMWJcdTAwMWOqRZx3Mki4p5Q5yT6/mvFwsXOyUlx1MDAxYdmM6IBJu9JsNCrlLlxubbxZO7HNPb51/dFQslx1MDAxNdSQXHUwMDEzsbPgiTHXXHUwMDBl5YjxbEl27zpcXLlcdTAwMTGlneqmvJO79Ydav1BdW4fl/OuwpXhhylx1MDAxMOWSMVxi1Y1cdTAwMTNKeFxmiVlQYWclglx1MDAxNVx1MDAxM6euwlx1MDAxY1x1MDAwN1wiLUiNiiG1i1x0WotqsNaOXHQ2lbpxM6HniVx1MDAxNPuPp+ekmVx1MDAwZV3+e4a3Sdy6oFx1MDAxNqfoZjqd3ac+WSv82D96Xj+65aayXHUwMDAzz+23rWo777qtuaXAV2mVpXWmUFNcdTAwMGYhrPFcdTAwMDA0KIe2OaWhzk63XHUwMDA11efQXHUwMDFhjEaK1jwuOiuq25JqxuKlM6nF+qXbs9HtxJlcdTAwMGVd/nu2d/K2JFxuOteWam9n1u2Ng9LTy718qrL6ZW//ar/fdLVcdEvYzVG3hd8ohHIrhFx1MDAwNlx1MDAxN3aqkVx1MDAxMT1O2oP8yFx1MDAwMsGh01dtrj2qwkJ9XHUwMDFmXHUwMDFj1dKLXHUwMDBivIzqNlfcooR8fnH0uVveP1x1MDAxN5OLlfLzZ1rdacOYpcXNXSRIatCInSEto7CaLHk/76rbZme8/va4fL66uX9cdTAwMGXLauPH5sZF7lWXXHUwMDFi321GY1uhytiQ1yyl8Vx1MDAxOKNkXHUwMDE2Z5WGmXrNnKKqUVx0pKXqY1lWui3VqHOSTcFl/lwi5TlcdTAwMWHcsfNcdTAwMWO6+rfsbcFcdTAwMTJdaYO/jG5cdTAwMWFkX1x1MDAwZSue2Ot+4fpcZnYqzf3e/v1b4/THU971XHUwMDFhLW3POIZcdTAwMWGLastHwit+2tvWs1x1MDAxNCVqXHUwMDE4zsNsXHUwMDE1e/yVbsFcdTAwMTRcdTAwMTPOwJe5vVCaPYeVbpGcqctcdTAwMTnlOmlcdTAwMThjpftod+vyfPf6ub++UrzQ6z9cbruVu9xb24JcbrNQzTghlFx1MDAxNCBCIXG+tS1RrSWjQEpcYo1rioqtmCdotVx1MDAwZd16XHUwMDAz8YtkUWubstesdnpcdTAwMWGbWFx1MDAwYmZtXHUwMDA3XHUwMDE2lz/b4v5oKDNd545GXGJcdTAwMGVrvlx1MDAxYqQril7IrMIrP1bqrNy9u9gsqL1m/2GnLpdcdTAwMWL5V2HpoSPMqFxiXHUwMDA3OFxilvNcdTAwMWGY3UA9rIGBhVl6zJMsdFx1MDAxYq4o8m5cdTAwMWFF37/IOd/r3H/9vOH3Uqt13MV3PZiz78/VSn85Km7/uPU/lLbhTyFpYMWf6r//+vv/XHUwMDAx8pX4biJ9 + + + + + QUICSocketQUICConnectionQUICConnectionQUICStreamQUICStreamQUICStreamQUICStreamReadableStreamWritableStreamReadableStreamWritableStreamReadableStreamWritableStreamReadableStreamWritableStreamUDPQuiche Stream Send BufferQuiche Connection Send BufferQuiche Stream Recv BufferQuiche Connection Recv Buffer \ No newline at end of file diff --git a/images/quic_structure_encapsulated.svg b/images/quic_structure_encapsulated.svg new file mode 100644 index 00000000..c3bf43b2 --- /dev/null +++ b/images/quic_structure_encapsulated.svg @@ -0,0 +1,17 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daVdcIktcdTAwMTL93r/C43x91uRcdTAwMTK5xHxcdTAwMDNX2r1cdTAwMTVR58zhlICAslx0uM55/31cIrGFsopia0qrx/a9091SVZBkxY17Y8ms/35bWVntP3cqq/9aWa08lfxGvdz1XHUwMDFmV/9yrz9Uur16u0WHxOD3Xvu+W1x1MDAxYZxZ6/c7vX/9859+p+ONrvJK7ebrlZVGpVlp9Xt07r/p95WV/1x1MDAwZf6kI/Wyu35cdTAwMWR321v5wt1ad+/kdr9i4Lt6uFx1MDAxZVxcOjjpbUB+t9t+XHUwMDFjvfxEr1x1MDAwMdfSXHUwMDAzZlx1MDAxNUdUTFx1MDAxOD48+kxH1zi3zJNCKKtcdTAwMTE5/SaGx1x1MDAxZuvlfo3OYZ5gQqLWXGJSgVx1MDAxNIzp4Tm1Sr1a67svbD1cdTAwMDYoXHUwMDE515wxwY0yw3P8VrXhRseGr/T63fZtZb3daHfdqP/BK+6/0biv/NJttdu+b5WH5/S7fqvX8bs0SaPzruuNxkn/ufE6xX6pdt9ccrzL66dcdTAwMTR+flx1MDAwYlxivT68rtzu9yvl0WX0udVaq9LrvVx1MDAxYnK745fqfTdl9P2Gr7pBdnLlwW37z2hYXb9Zybn71rpvNIJv3Cr/fOO32zu6d+LnK3+PxlmpuPfQUqM2mo1uzMjMrGDhV1x1MDAwZtqtgclxKSXnXHUwMDAwgVx1MDAxYlrvbZCd9Vx1MDAwN+967Td6ldGEu6Ftjmzw3bjvO2X/9VwirlFaplx1MDAxOd1hPfrgRr11XHUwMDFivqbRLt2O+ZxOu1x1MDAxZTRy9zP618poulx1MDAwN79cZv/9n7/Gnr02wTDdT9QkR+/4LfTOq1xyv9dfbzebdWdcZkdumOGv1Ov73X623irXW9X3d/AneFx1MDAwN3d8tdmDp+aLOVC3XHUwMDFi9qZ31dup7lxc3q+OxrV63S7dXHUwMDBmbMtjhDppuFx1MDAxYlx1MDAxZI3dXHUwMDE4XHUwMDEzOKvqd+hcdTAwMWP0hDRS0Y9lUoOFiJFUWuXpYzosnFx1MDAxZKiGv5/L3Z83N/eRrV9sXHUwMDFlj1x1MDAxYlx1MDAxM00pk8KANcZqXHUwMDBiyFx1MDAxMDAyKOVcdTAwMDFcdTAwMDchaNhGam2QRy3XTVXG+aJaxY/ggIZcdTAwMWM+9i1w/dDzvTm1bqXUf/Uh41x1MDAxMCBtXHUwMDFjXHUwMDAywChcdTAwMDVAUzdcdTAwMDNcdTAwMDBevSxcdTAwMWNcdTAwMTf8o7WT3GW+vLaTKe7t9Td2eou5XHUwMDFiXHUwMDFlen14Xa9Nvn+8t1x1MDAxOV0z3ttEXHUwMDFkqXPw0qD2UFx1MDAxOcatQWOlUCFcdTAwMGZcdTAwMGbaeHQ7XHUwMDE550owRKlCQ/t1P/zGXHUwMDE0XG65h1x1MDAxYcBcdTAwMTj6XHUwMDE0YfTI9VxmeUIp7ZHRcFx0hkZKJ4XdXHUwMDFk55JcdTAwMDHBNXDxXHUwMDEyvayM2GrY9Vx1MDAwNT7h11x1MDAxZN9Eu+5XnvrjTFqiiTNpMmZccsqAntmkO5lcXPa0KPxcXOmiUMhf/Fx1MDAxMO2WPl3MpMVcdTAwMDImPY1AY0zaXG7lXHRcIi8yXa24tTZs0lx1MDAwMFx1MDAxZVx1MDAxMvkhXHUwMDAzg2CETsykyVx1MDAxYz2r0FwiwcxaYflcdTAwMTibXHUwMDBlIG5oxETZ5EfNXHUwMDEypMK7XHUwMDAziVrraFTtVv+k/jKQJezdq1t+s954fuerXHUwMDA2ZkzTeJzPrZ9UumSxq++OZVx1MDAxYfWqM+vVRuX6vb336ySEh4f77c7oaIk+y6+3Kt3o3LS79Wq95TdOJ3wufdfKztvt4V7gXHUwMDA2Xfm9ijvqXjeL045W8cJLMHLGkmxmdpC29b1cdTAwMTR32fZa/ba4efRSe7nGfOp5xzL0tLLcXHUwMDAwSKNcdTAwMDI681x1MDAxNaNcdTAwMDKIdoxQwLRcdTAwMTWIQiaHUSAlaIB8XHUwMDAxRTlKXHUwMDA37vZcdTAwMTCi8af8RKwg4jTMXHUwMDA0QpuPZZ3hNaOr46hcImA4O1x1MDAwN9fG5DpcdTAwMGa17dPC6Xl187B3l7lYXHUwMDFknvd3ROZ+XHUwMDEypWlcdTAwMWSr0uhuXHUwMDAwk0BcdTAwMWF9ZriM/9aph4vgXHUwMDFlheFCXHRS1lpFZFx1MDAxYUfmcdL6XFxTpFwieHhky6Q0Q1xuniiNPs2A4Fx1MDAwMXadRGlaIGn+4H36XHUwMDFhlOber1x1MDAxZkNpJVx1MDAxYXOQd1wipNasl8tBXHUwMDA2ec9r01x1MDAxY/9Yqlx1MDAwYo1nXHUwMDE5VFx1MDAxN1x1MDAwN1tcdTAwMTI/OvzyXHUwMDFiboVGMlx0+plcdTAwMTm2az6o8yffXHUwMDFlbl5ky3u7l/nu3v5V2qUoUNjkcUCKVyi2Qlx1MDAxOKWRXlErjfG0lZxokNShtFx0spziXHUwMDFlRXiW3IOShlNoPlx1MDAwYmxRo9Jaw1x1MDAxMqKpX0OtXGZabOKoXW+3WqTfnFx1MDAxOX+4XHUwMDE4XHUwMDFk99mJXHUwMDBiUi5QhV9cdTAwMWUmQpRUZL56dqhOTmKlXHUwMDE0qlx1MDAxNDejR9NA4ZpWaHQ401xyjFxi1lwiIFx1MDAxN1x1MDAwNFx1MDAxZsWTgyrFip5cdTAwMTHWgiTgXHRcdTAwMWHM6KNGUOXMI0pVYInsLY04QrhAOoCi35RJ0lnqXHUwMDEx78z2tSYxtyadx1/8XHUwMDAybFx1MDAxOI/k1YfKlFx1MDAxY60hsYSzK9P+Xe7q+maz2TRXhe7Jj97T5nn9Lu24IVxcXGLPXHUwMDEw1WvCXHUwMDA1qVNcZitTKdFjxlx1MDAwMIHKOINOLoFIg3AjsVx1MDAxNiUjXFxcdTAwMTg9XHUwMDA2OJxbz6X6lVx1MDAwNKdGhY6mXHUwMDEwXHUwMDA1cEkqO1x1MDAxMIGnXHUwMDAxOdOCue5eNafq31x1MDAwZrbWdm+6z+tcdTAwMTX/Xu5cdTAwMDSDub/Gv+3rxSdcdTAwMWJ3orxh/br9cd9sbnO+ZteL6Vx1MDAwNGSs2qToKFx1MDAxNoua5KZkwGdPqoyfztRjkWhcdTAwMDGQkWRTxpV+ZFxii1x1MDAxNIR5xOWESWYkycBcdTAwMDT1JsWrXFyhtfRcdTAwMDc5QT1jmFxi5CSkxYA6/STBXHRz2OtcdTAwMTLCxH634jdcdTAwMTNcblx1MDAxM6fQytgwMTSexFx1MDAwNSjG609cdTAwMDKuQFeGXHUwMDFk3ZKp4O1uZO9cdTAwMGK5R1bstPud1ka2rvZcdTAwMWKpXHUwMDA3L7fWozCQI1wiXHUwMDFiVXPfWFx1MDAxNKRHWlAw4jUlMUhb6WRR42p0uIxcdTAwMTLGXHUwMDA3kuhNo/pQeum8bHfXStVS/lxcnFx1MDAxNb+30kOi8zilXHUwMDA1SVRjLFx1MDAwZTlcdTAwMDVcdTAwMTRI93v28uH46Uw9XHUwMDBlUXtKoWDSaM11WM9cbnQ1aLCWKSMkqFx1MDAwNPXsQlx1MDAxY4pKapuGnM3/XHUwMDExhU4hlI+i0FCDXqDozy1cdTAwMGa/PKooktgzXFzL2Vx1MDAwM9HJnuzzXHUwMDFi5+KAq5hnXHUwMDA1XHUwMDAzXG4yXHUwMDA1wTOcwVx1MDAxMcZ4oChO4EDaN5jxWjZwhaVcdTAwMGZClILonFx1MDAxONKMYVDm0Vx1MDAxMass3Vx1MDAxZGXB9ZdcdTAwMDVm6lx1MDAxNchWK2ZcXFx1MDAwNihcdFxujTZcZn5cZr5n7IObrFlX3vXBSe6iXHUwMDE5mkSSTVx1MDAwMsjSXHUwMDAzp732nHHuSVx1MDAxNFx1MDAxNlx1MDAxOVx1MDAxOYclty5cdTAwMTfshJvsXHUwMDA2XHUwMDAyo1pjXHUwMDFlmZuQnEkhSSVJXHUwMDFkbYRDTzFuXHUwMDA1uOydXCJNZ1wiY5qxq3CeVrnhsaX2Usba++DqqKWP3u9b8O/5XHUwMDBiTPEhv7UkVkiAzu70Kk/Htqj9XHUwMDFm29vP11x1MDAxYudcdTAwMWLnYu3k5nvqxYqL+DVFzNxVwumvcNzAXHUwMDA1eK6bTpFgcdVhXHUwMDFiXHUwMDFh2mdXmECQplx1MDAxNFJ8MbXy9SpMXHUwMDE0XHUwMDBlxitcdTAwMTREsMDnaEyc3JOcUrBSOOxcInxF9lx1MDAwZeQlKbpcdTAwMGWDXHUwMDE1tGtcdTAwMTdkXHUwMDFhiKNIoSRcdTAwMDfWJdWYpDGWpSxR/lElpqVcdTAwMDXjU0pMOl7ZK8BBXHUwMDE2d/bMWDlvXHUwMDBl93JH36vbt1x1MDAwN1x1MDAwNztF9UOerV2nXHUwMDFkN4NcdTAwMTJcdTAwMTOF42SogFpEOI5Z5bluWSmIfzhLsPdpOakxUntcXFx0+Zvlxko9Wf1RX3/pZlx1MDAwZdjhw+b1U60o1ay5sdPdvfuLXCJ0XG6tfLVcdTAwMDHHuez6TrecTjhOaGeKz1G7XHUwMDBlXHUwMDFmp3pmR+L46Uw/XHUwMDEy0UPLhGRSgbaB2PQnXHUwMDE0JXpuyVximb7RXGZcdTAwMTKMsVx1MDAxN0uOXHRcdTAwMWGcXHUwMDEwXHUwMDE0ivwpMC0vOzaFVFJRYOJcZmLRqyzjmuk5mlx1MDAxMTNXta6u6c6F36ic2eZOo/rCXHUwMDE3TJB9cIFJXHUwMDE4XHUwMDAypiS0aIjSKGGb/JjLkFhmVHhkqeNRy41Ct0zvt+LRvHjJZV/yN7Xmlc5cdTAwMTcubfFJ3vW+XHUwMDE0j05YdmxAa3LRMDtcdTAwMTTHT2fqoYjaY1x1MDAxNFhpo8BwXHUwMDFlXv3CQHqSgkQl6Vx1MDAwNGLbXHUwMDA0q71cdTAwMGL2aTBDoaFcXMYyyz80+pNGp5DK51x1MDAxN5lkfD+/XHUwMDE2Rmqp2Oy4nezJUl1jknSeMz80o/Xtb8BcdTAwMTXgkX/TykFcdTAwMTdYclx0nOWUmNBpdStUXCJcdTAwMTmclJeYJqvWlU8qMU12XHUwMDAyK39KTFx1MDAxZltiQj5Bq6DWllx1MDAwNjV7zL+/08n7t41Le7C2v9tu9e5uXHUwMDBlT7Np1ypuj1x1MDAxMc+tXHUwMDAxceE8Y1x1MDAwNsJNpUZoTyFTZPNaWZtg2LBQhYm7KENcdTAwMDVXXHUwMDE4f1x0qfL1KkxBU440gEu3cFx1MDAwN+bAaj5/tFUy7c7O01x1MDAwNiuePu6ePZ482sWw+oHLhFx1MDAwMSmGd4vmXHUwMDE5Ki65XG7n59zWXHUwMDE4nIxSu9miXHRJXGar5FxiPFwiTcul0UQ/bFxmVIm3PEZAVorCXHUwMDBlQyCNyFx1MDAxM+mW/jukJyFPfrHAdF3s3X7ffbC78nnPft/yt0y1+fx7ReImvtlcdTAwMTMtXHUwMDAw0ehcdTAwMWMpsfrVXX+fWcGzzbvn46Ob9Zbgqa8sSSWFZ1xmKXZcdTAwMGJcdTAwMTRm68hWMZZxz5Cc16Tp3FxuweS4jWnyiUj6keDLWVx1MDAxMLmT0tlcZlx1MDAwNdlcdTAwMGLjXyydvd6ov5vFXHUwMDBmI7bQ535A2jpcdTAwMTC0hUlccoBcdTAwMTCq+Fx1MDAxY1xy2YXdh+eTvct25+5cdTAwMTZcdTAwMGLt0s39y0N1P/Ug1Vx1MDAwNj1tOXLtQlWM6E9Aj7nMlYNcdTAwMGVLsOS0nKZcdDqkNVvGXG6nRDYo+5i2XHUwMDA2Y2NTSZZzKyWbY0eXSrbo73a767ly//xiPV/aXHUwMDEz7Vx1MDAwN5Z6qzaM5Fx1MDAwZqLhLvlcboyHpVx1MDAxYWlcIo9o2CruYkyt016OkVxi1nVgpKtcdTAwMWJoWjXmNpttZ/erebW2XdhcdTAwMTUnpoN+tThrNeYl85DHx7OXZnnrSFx1MDAxZerMU15fvPxeXHUwMDFh0KrYtjxDTpWb4H6904A4fjJTXHUwMDBmRHLZrlx1MDAxMuM2S3XJhTBcdTAwMGWBe9KCXHUwMDEyxLb0b0hbLVx1MDAwNp2d0NCWsEzgd5KAiZZipjBKKjpcdTAwMWEmRG9cXCkrufPSM0M307y9O+o/lp9vntXh2eVNXHUwMDBm9oqp33vCUZY3UIXGpedlpDNQo/C4XHUwMDExblx1MDAwN05FXHUwMDExLUsu37GsvSdIzVx1MDAxYoMpa6mdRqKnnY2Hu9z+3iE0z+D0aK9wfrfOv1x1MDAxMolCfI+uXHUwMDE05MqNUrPL2fGzmXooun2kiSUptJFCYJRFlaeMXHUwMDEw9L9SdHLKSNQyqSxp1y9WJEi2n2EypXx6P4NcdTAwMDBcdTAwMWSrfTWjSIbJOfKfk91YWvtcdTAwMTkkhZfESVx1MDAxYVx1MDAxMCl+Q1x1MDAxOc6taKE9V1M1JCeImGRyQehyXHUwMDFhXHUwMDFhuNuqnoHhX7GjYbJqXYnvaFxiXHUwMDA0gStLb2iY6Fx1MDAwNVbiXHUwMDFiXHUwMDFhgFlcdTAwMTlcdTAwMTnV+45cdTAwMDZcdTAwMWPz9Ig/XHUwMDFkXHLTtvhcdTAwMTDxXHUwMDBi8ThHo9+hZ+pej6VWp9g6uoG158z+0c02v6yub6RdrLh1a551fK+Y1JaFu7hIv1PIr1x1MDAxY2BJjEPa1sySrCGIfHbEL4MpyT9cdTAwMWRccol0NJh4qFqJ84VcdTAwMTXbWbnN1nZcdTAwMGY3/HzWz6038rlcdTAwMWa9XHUwMDA1N+P5uIZcdTAwMDZwXGJxm1x1MDAxMJNYk9wyXHUwMDE23o2HfKeHdFxiKLhcdTAwMDCNLLlcdTAwMWTtXHUwMDE0XHUwMDAyXHUwMDA1OOj604VcdTAwMTKcj3lOwPSOXHUwMDA2TtczLWxgM5Y0XHUwMDA0+Fx1MDAxZtHSwFx1MDAxOfHYXHUwMDFjXHUwMDBlYyF2Mzb+gU6WS+R6jn2MK3h8eLnxTIG4YFx1MDAxYlxyjpnyxWY19dzGrfLAPY9cdTAwMGaQpFJk+yqDwnPLZsh/MKtAJZhcdTAwMTJbqKWBkzKlgIR9taVcdTAwMDVfp6WBY+ymyW55pnFcdTAwMWVyjp0g1us7jZ2z1rnJ7mSL0LvOXHUwMDFj7N6kXHUwMDFlpFx1MDAxMjTFYEqgRi6IXHUwMDBmwiBcdTAwMDX03Fx1MDAxMzZcdTAwMThcdTAwMGW2oOPJgXQpPVxyXHUwMDEyXHUwMDE1xZKftp59mTJ1cbtcdTAwMGVqj4hYc90pXFzM0X668aD2LuD29qrT2d29z97Irdrdglx1MDAwZnX6SLPWSEEtmS2SXHUwMDAw4jJCPtptcUpcZkQhr+FKitRv1Vx1MDAwMFx1MDAxYVx1MDAxY0+ma1x1MDAxM/1p5ZjWmq7cnlx1MDAxN5rbW2fdQ253cLdaeZi1XHUwMDFj07+4yt1cdTAwMWT0yoh1PFx1MDAxNsdXXHUwMDA1VcvklleOWSZcdTAwMWNjXHUwMDE3bZj4pd7CMMu5nD3BMX4yU1x1MDAwZkRjPOGaqKRcdTAwMTU6+PTft2qMq4tadE8zs2BcdTAwMTOE4WLLS4mIJFxu+GJcdI5EqzFTXGIlXHUwMDE1LVxyNr4rkFx1MDAxYuY2XHUwMDE0YTM9kPpcdTAwMTW536+zj5c3J881PD989I/4rixnd1KPXFyllKdcdTAwMDTX0nDNVLRcdTAwMWKJwjfUhlx0ZVxmU0wnuIP/Ulx1MDAxOFSQXHUwMDBmUszIlK3hmEah5cpz5n7nfKtWXHUwMDEwZ77erV+08fzqK1Goim0ucptMWkcsM1x1MDAwM3H8ZKZcdTAwMWWIWnpcdTAwMDKtdqlcdTAwMGLS9lFcbtVcdTAwMWXdXHSNyLRJ3y7gXFxIXHRS8mV0XHUwMDEy/eHQn1x1MDAxYzqFUT6/o8HK+NVcIlx1MDAwNjjB1syxWeBkP5bWllx1MDAwNlDcuP37ravc8eCzY99aXHUwMDFhXGbhSViKUYW1XHUwMDEyk1x1MDAxM79LamlcdTAwMTCS/Fx1MDAwYiSzy9GvdjRIXHUwMDFi4IHldzRMXHUwMDE2rSuTOlx1MDAxYVx1MDAwMieNa2hcdTAwMDCOka8+Uz/DZFx1MDAxZrAyqZ/BRFx1MDAwN/Vnh4Zp/Vxms5Sm4jzj65aFKD1Nt1x1MDAxZIBcdTAwMTNcdTAwMWZGl49pkFx1MDAxZXGre6RcdTAwMDcwXHUwMDFhXFxcdTAwMDTGiq43UrtteZWxOrBcYnRcdTAwMDTjXHQ+aVleJXF3O61Cm6jbXHUwMDE5hlx1MDAwYlx1MDAwNjSzgJEy3upgdXssu5FcdTAwMTIjXHUwMDAzQzOLKlxyu7h3XHUwMDAz//XCZTLwirXC0NXfQu8yq1x1MDAwM5nu+SbviDB0acqjOJ3mXHUwMDBluEBO3iC4Lc3IXXMh3MPDXHSUTCqj7WLOeHJPw3BI4ElJXFyPlmhfyMBcdTAwMTYlIXdNsoCmVtOfglwiX1x1MDAxMe14m9vVTtRxXHUwMDEzcyHaxOdCXHUwMDAwXHUwMDE4h3fdytOk3Pn99UP+9qlcdTAwMDfa1C7Lm5mjXHK2UUh784dUgnjDrXE3gjxzpJLNwVNcdTAwMDRQY1xyXHUwMDExLZhcdTAwMDRcdTAwMTdIXHUwMDAyjcNcdTAwMDBcdTAwMDNjXcZUjGn9iD9l2PnhNtwwJmWdXHUwMDFm01x1MDAxMiH7Xdtq711lgb3swOZ+rviQyXRTuFx1MDAwNENjfOQjhNVWqzk2Z1x1MDAxZf+tU1x1MDAwZlx1MDAxN8k9UjGMUbQgMJKwUG6BXHUwMDA2KVEyUY2BXG770tFC8Vx1MDAxN8lrV79Wxjn52Vx1MDAxMlx1MDAxNu75rK6H66utY3TvXHUwMDE31/fxq1x0iyluf2zCXCI0nsST/lx1MDAxYWNVnWBaXG5S5nb2dpC7q/x2T15cdTAwMWbRbbpqXHUwMDFk6vPzy5et7bQjl1xmX3vW7c+spFx1MDAxMFxcR/qRielAWUvKT2FcdTAwMTBPaSQ6ZeVg16bfa1x0Izn4fO3Z9PfV3k1xM9PP1Cu13Jw8p0xgNpJqcOTxLcGau75cIj3Hzlx1MDAxOeO/derR4vYxd7uKSaVRyMhcdTAwMTPnXHUwMDFk01niXHUwMDFk4TZcdTAwMTJcdTAwMDGW5DOvXHUwMDE2ojq3RMnVMj+3w3FOa005003x+0tnum8/fcGq3+mc9Gluh+5q9aFeecxGXHLpXHUwMDFm14NcdTAwMWbnU1x1MDAwNqB36KpcZrzc39/+/lx1MDAxZj8gY3IifQ== + + + + + QUICServerQUICSocketQUICConnectionQUICStreamQUICStreamQUICConnectionQUICStreamQUICStreamQUICConnectionQUICClientQUICStreamQUICStreamQUICConnectionQUICClientQUICStreamQUICStreamQUICSocketQUICSocket \ No newline at end of file diff --git a/images/quic_structure_injected.svg b/images/quic_structure_injected.svg new file mode 100644 index 00000000..9dff0683 --- /dev/null +++ b/images/quic_structure_injected.svg @@ -0,0 +1,17 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWVNcdTAwMWJJXHUwMDEyfp9fQXhfR7WVdde8mfs0YLA5NjZcYoFcdTAwMGWETiQhITbmv2+mwKjVl1x1MDAwZUvQjI09XHUwMDFlW63urq7O/L7MyqP+98fKyqfuoFX89NfKp+LjTb5WKbTz/U9/0ue9YrtTaTbwkFx1MDAxOP6703xo31xmv3nb7bY6f/373/lWi43OYjfN+vOZxVqxXmx0O/jd/+C/V1b+N/wzcK928aabb5RrxeFcdMNDo9tcdTAwMDHnOvzxl2ZjeG8wTkpupOav36h01vGG3WJcdTAwMDFcdTAwMGaX8rVOcXSkMLzX7XadXHUwMDFmnrUqObHB63u7+8WvR4PRfUuVWu2kO6g9P1n+5vahXHUwMDFkXHUwMDE4VafbblaLZ5VC95buXHUwMDFl+vz1vE5cdTAwMTPnYHRWu/lQvm1cdTAwMTQ7nbFzmq38TaU7eH7C10+fp+GvldEnj/gvXHUwMDAzXjCnrHTWam6UfD1K5+dAesOkVYpr5YUzMjSwtWat2aaB/Vx1MDAwYor0azS06/xNtYzja1x1MDAxNF6/023nXHUwMDFinVa+ja9s9L3+yyMroZn3XHUwMDFjhLfKc6ONef3KbbFSvu3idzRcdTAwMTimjVx1MDAwNMlcdTAwMDVXYKxcdTAwMWGNpjh8LVx1MDAwMoTzjms/ekhcdTAwMWFDa6cwlJH/jl5GO18v7tApjYdaLTifjcLLfP6QpZE0yZdP/lx1MDAxZT0kfX8jLIVBSVxmyMfWbrtycNm1p+s73586sH612V49eJ2IMbHNt9vN/qfXI3+//G00/odWIf8siWC8xCc23HE/mrJapVFccj9cXK15U1x1MDAxZFx071x1MDAxZoEniahNt/jYjdNcdTAwMTjDTZLCaCW4XHUwMDA0XHUwMDE1eCeT9OXydLP5YMzD/ulq+7ja2Vj/5s9359NcdTAwMTcxh77w+fRFXGJg3oHy3kqplFx064vjzFx1MDAxOOmMMFx1MDAxZayWbmlcblx1MDAwMyCZ0955ab1zwoGIKozQYVxyXHUwMDAxVGWHp0n181x1MDAxYTJ2IKJcbotcdTAwMTTW0aiaje5J5Ynei+Bjn27m65XaYFxmXHUwMDA3h1KM03j8bWftpNhGgf00duxzrVImqf5UK5bGxb1bQbJ5PdxttkZHb/Be+Uqj2I7OTbNdKVdcdTAwMWH52mnKffFZi9s/Xlx1MDAwZrDAXHUwMDBius53inSUPrep2plKak5B+NNcdTAwMWYqqoyW2lx1MDAwN2Vykop+P6lcdTAwMTY7/a1q+3ptq5PLPTxe17Y6mac0YVx1MDAwNOOGe+4kkpq2IVx1MDAxNTVcdTAwMDBMW8uVM9ZcdTAwMDO3NjSyxamoIDDQTlx1MDAxYS6sXHUwMDEx+JeohoJcdTAwMDKmLKpcdTAwMDPyr+Sai1x1MDAwMGC8aKxUXHUwMDFlXHUwMDFjjnp0JKixI5Z6PNnoVKv7XHUwMDFks1r98tQ/Pdt9XHUwMDE0ne5cdTAwMGZcdTAwMTL5sNT3Z9p1y+JLqXHHv29t6JPC9nZcdTAwMGLuu538XHUwMDAyrrtbre3liqv9/l4ut3ZS6+xLuP9gVG29SsJcdTAwMDFAXHUwMDAzyVhAqZxcdTAwMWFcYnKdb+XTw918Tpd2V493V69aXHUwMDE3jVbmuZqDZVx1MDAwMvVcbk1XaZ1cdTAwMTIhIHBo+irhLFqcXHUwMDA2TWCzPCBcdTAwMDBcclxm74OAo7W0wFx1MDAwM+8mjavRyjBcdTAwMDYhLF7z35Cr7Vxm0vrzXFy91mw0kOJIjt+cr+PuvXTO9irRrFZeOeeUnMFccs3tfeZcdTAwMWLX6qYjeudcdTAwMWTNa8d3tbWsczaaJYZcdTAwMTmnJVxmWVCP9ONZVdEzZF5rcFx1MDAxYZBLwZnQyFx1MDAxNuiHSsuQlDViXHUwMDA3XHUwMDE3aF3HWNVSSMZRjzXCKGq15Fx1MDAxMT/UelRcdTAwMWJcdTAwMTP0XHUwMDBlsuOGLoszr49qnVa/f9XzhV1dN98ueb97s1DOnFx1MDAwNYXm4kxcdTAwMTOQ0zBnasD3XGZ2etv55HCzd1Hi61x1MDAxYqWL1uqJa1dOL5u5rFOmJkVDKkS51lZDwOZ80UPtXHUwMDE5fu6EXHUwMDE0zmgrl0iZ3CDS4nCE11xuOOiY9aBcdTAwMTjKRJq3qHduXHUwMDAxmvehKLNWXHUwMDE5m8U3o8vQfZdPlVx1MDAwMe81rKKCOy/99ExZub9vXHUwMDFjXHUwMDE1mo3T4+2NR35UXy9Vc83sa6jTaLVKh24sSGNG8vCioaCYl0iTXHUwMDA2KdU4vzxccpVcdTAwMTaxQJBxYpRcdTAwMTBG+1x1MDAxOPdWXHUwMDAwesCkwc5cbrS3TVRhhVx1MDAxNlx1MDAxMlx1MDAxZkOMZjA7VOlcdTAwMWa3XHUwMDA3qFx1MDAwZX1j28frRXvXbzV2qtmktFSlMTaR17zEXHUwMDE3gGA/vdZsVyu1c/H5+uvFZ1fPrZ7Xr1x1MDAwYvo261qDzO6YU2BcZv7n8XfYXHUwMDE11JIhqXGrlVx1MDAxMVx1MDAxY1/N8nhNXHUwMDFhZkE6hCqOSmFN3KJcdTAwMTA4Jjg3Wipu0OBccnqmL2rDlfVcdTAwMWHxblx1MDAwMcu4XHUwMDBiVJtYXHUwMDAzKyA7h0fN9lnpRPa379bFVb2+ffx9szitgTm43N24+7xcdTAwMDWrt/v3/btcXOl6/e6in01tTFxclFGJimg8ej1ai+nXZOInM/OKXGLo6KFcdTAwMDNnXHUwMDFjLYpKXHUwMDFkVkRumVx1MDAxMMJcImFoz4NcdTAwMGIlXHUwMDBiV0RcdTAwMDFcZpBNXHUwMDFk/lx1MDAwMeiwOVx1MDAxN1XEXHUwMDE4XHUwMDAzXHUwMDEzXHUwMDA351x1MDAxMEH4XHUwMDAy+OojXHUwMDE5mCfddjFfTzAwb3DMwSBHxMSsV1xuhSA1jVuZkyglNq5cdTAwMTJcdTAwMWHP0lxyT1x1MDAwNOMk1XVguVx1MDAxM4HQ6CTNvb85fvy6tnVVO9wxZ259Z9V3VruZ11xc8GhZouBzpDDUmSiFOsZcdTAwMDEnQnjkrIBiZ5NBcZjeiY9GoL2HwnnzvPvUa9z3SnmX+9quNiq/XHUwMDEygYrkqIbUxuHr9tMv0cTPZtb1UHDBXGZcbj5HY4FEPKyHXHUwMDAwTKF96I3Q1ju5vKXSOVx1MDAxOVSid6p+vajGUlx1MDAxOXRcdTAwMDKjvFx1MDAxNYM+w0aM3lxuXHUwMDFl4Iuw4lonlLJOT586lI5kMylu2MB8Pa/Q7NJcdTAwMDBcdTAwMTbKoNowr5xS0jhcdTAwMTFOS7BeMPDGeFx1MDAwYuClsT40sEWmJTCF/C1cdTAwMDVOPFx1MDAwMqaN4U/O8FxiMjpcYqmdXHUwMDEyYHUkxiGlXHUwMDExhqNBsFxmXHUwMDAyXHUwMDE1k1x0dCna3enm293VSqNQaZTHXHUwMDA39pJrOo3BOsSDm4ehoDBO6YpW4iR6juzldeBb5XxraKsw6dFcdTAwMTKhpEZ0iqSNPHuxUZg8pnRcYlxijCnHXHUwMDE5XGIrJFx1MDAxYWlCooUkrYiMyTPNXHUwMDAx1ZKW7LRHZo2MqZbvdNea9XqFtOSoWWl0w1M8nMvPhFxit8V85P3jMyVcdTAwMWVr0eXGraTR31ZGWjX8x+vf//tn7LdcdTAwMTOlfXh2VM5H1/sj+P8w5E2T2JKEi0MwUFxcM+0kXGKtQdtcYlx1MDAxYyiumPDE8KC1ds5G1JhcdTAwMTKHXHUwMDEwNpXmxirtPEBUj8kmd1xuQYXiKPjso69EIWpRMPPeZtNSYehcdTAwMDf44VxcXCJOu9H4g2nlPjGtnMCdIzZNY6OGXHUwMDExb2zcXHUwMDBixLyFaluyUNJPLlwij1Ftm1x1MDAxNWEmI2N6Yv4r5ln0bq31ODpER+1FXHUwMDEwXHUwMDE1XHUwMDAz2ImArr3lRnruJXqhKlwiI1PBdXpqZWBQXHUwMDE2MVx1MDAwMO14ZbVcdTAwMDExRlx1MDAxZsFBKUO5wJqyXHUwMDFh0ZaKiu0sWIxqXXmGh1x1MDAxNORLTyNIRT5wiiHdof9iXHUwMDEwcnU4hdqQkHhcdTAwMTQg0NJpnOdcYvJpzySlr8FwOcKokdi/XHUwMDAyn7d4XHK8lNVcXCmN9tZv4FtcYvBcdTAwMDGt70hcdTAwMWTM81x1MDAwZiCfXHUwMDBlIFBcYvmsXHUwMDEyXHUwMDAyOc1PY+R/SOTzXGZQqJW20lx1MDAxYlxuRavg2UoyK3HSJPK5lta5SVdLXHUwMDE08eGtwsL9JiiamldcdTAwMTawXHUwMDFjXHUwMDFkYqOxlGhOmu5cdTAwMDPfXHUwMDE4WcWAXHUwMDAzXHUwMDA3rtEkQ7ePXHUwMDEypZZcdTAwMDSiXHUwMDAxiDRUr+G1pfieUEJGRlx1MDAwZZbhYFx1MDAxY2K60IBumo2u4i1cdTAwMWVG0zONU2HUSE2hXHUwMDEyoXFcdTAwMWXJdFxyw6hyTFx1MDAxYUPBXG6pZOCBX2HUUqhcdTAwMDXtTyru4lx1MDAwZWSMXHUwMDFmiD6pXHUwMDE33jmNQu1cdTAwMWSMPIXfMPp64blgVHDJqXYnXHUwMDEyXHUwMDBmXHUwMDFjOoRcIrkuXHUwMDExpJZcdTAwMWNdtmmiXHJcdTAwMWZcdTAwMTJHUXKZXHUwMDAzhFx1MDAwN+OoVMlcYjGGpFoxsIh+aFx1MDAxMFx0XHUwMDFjrp6IpLlEMaefiIC/XHUwMDA1lFa3XHUwMDA3+buzXHUwMDBiude2uap6KvYv7sS3KJSCRO+UU9qh8IpUOM74Q9tcdTAwMGbNQ0Mp7lx1MDAwZZ9EoStcdTAwMWKVusWCKWeaKjBcdTAwMTFb8FxyoHVcdTAwMWGHpVxiTTSb1iFMgTdvYZGmJ6BO8MVRqoSTZGtrkOHQlvX4OFx1MDAxNlx1MDAwNdI7yutccujeq0VqmEbnQVxunH9i65j8fiBvX6H+XHUwMDAyOj7GazfS8Wxg6VJWQt9cdTAwMDRMvdeC7K44m9TbRJPUUCFcdTAwMDYp1j9cdTAwMTVKXHUwMDEzpZJ+clGBzIRcdTAwMTk5Qlx1MDAxOaFcdTAwMDD9XvCUb4x+rY6BXHUwMDE5ziStj+EjICdcdTAwMTi03OaDvomAXHUwMDFjXFw7pVx1MDAxY1x1MDAxNTK70ci10srYXHUwMDA1XTQgUVxmuPdeXHUwMDE4rX7SkKRjKbh39O1cdC5cdTAwMWFcdTAwMTUobDx112q7l/nKdWszinvjIdXnSFwiUiM+i0amRTlQ4aJcdTAwMGLtPVx1MDAxZbZcdTAwMWNFxyPaR/1wQJOZ3lx1MDAxMVqYQ1x1MDAxM3RCXHUwMDAw8DfMvV44PoL5gme0gqPRXHSJdbEhMVxyXHUwMDA1aC1cdTAwMWa8yNTi4lhcdTAwMDSUvlx1MDAxMnxrs4ZS58poXHUwMDFmi16+fjprXHUwMDE5d2jokWBpin7yp+K3m9zXs5xS5SvTlF1xVotx8WKyj56VlFx1MDAxYXRog06y1Fx1MDAxYUS4XCJcdTAwMDOPMI9+NGVcdTAwMDLgYYjaJlwioIKvalx1MDAxOWivkVxytVxcRq3ZUjN6Ri1HpLNcdTAwMGVijVx1MDAwZnCJ6SpKcfTbLcxjfMyZPKS3uSt+aT/eq1bjqlxcva2v1Xxt2uShn8iFT71u6Ybv585PK4eV7f7Bfe/kfHevn1vAdU/uXHUwMDBmWif5r8eVwpP/vlnqXHUwMDFk9M2eXsB1V7fK31x1MDAwZkR/83ZzXHUwMDBifVx1MDAxYlx1MDAwMcdcdTAwMTd7O1fZSqJKe9vTmFx1MDAwNVx1MDAxMs0qxFx1MDAxNSvRV+ZChNfntTLUsiBcdTAwMDVx0KBkktLrXGZXaFx1MDAwN8Wtz2fOLMhcdTAwMTT+xFlcdTAwMDWA7lxuNdkxkVrhIX8mVy6iXHUwMDA1SuWsmVoxii/3Psi3XHUwMDE2YVx1MDAxZMycTDWNiTCJxtOLyceebDrjYVIqJFJO4iu3aEByp2Zo8LD57es5jusg/3Rt+menefWY651kZ002IaFKS8Gstl6BhWCp9ksmpDNcZoTSipJcdTAwMTQpXWnhXHUwMDAw83P9XHUwMDFkXHUwMDFjXHUwMDFmplx1MDAwNclFXHUwMDAzxsypUi4omr/bO8TjQqqSptesmkiayGvpgOCSXG65plfU9KWKd6e8XHUwMDA0RVVWMPRPPaW+XHUwMDE4kJGiVaeY0dyjryOFtktt7+CY85TyZaTRXHUwMDE0foxq6uT+XHUwMDBloKgxolx1MDAwNZ2t6oGfbl6Uavkuv8HDLDg0XHUwMDE3Z5rkwJpXaKeCn6H+rrt5MbjZLdVcdTAwMDZcdTAwMTdrvcPBl4PHSnFjL/uUyTXz2lxuh+6nXHUwMDAxr8PlXHUwMDAzXktG60/GKjBKZq7Dg0NR8UrpX400f5lcdTAwMDZcdTAwMGYmeYFcdTAwMTMxW1x1MDAwMthcdTAwMTlaPHztXHUwMDFjXbdXT+q7pY1mt76x87TPe6tcdTAwMWZARznzw9RcdTAwMWXtlY2EIz13zEhOvZCc0SrrLVx1MDAxZaxCXHUwMDFhVVx1MDAxZbLIlT+x+vTWnJZenZqcckx9elx1MDAxY1pe05enXHUwMDFlrH7mXy6bl1x1MDAxYXLyoGL2jkvl+pyNrN9SaYRiktKhXGbqj1DhdCinLNPoXHUwMDEyUlx1MDAwYiOkeb7EwvLF1KdyQc5hsN9NXHUwMDE21GbSXHUwMDFh8zHcmM5F8WL99qayKe43oLu2W57WwlxcO+yfn7d0pdq+6ue33GAgfWnK1mRZsTBtdHlutCojXHUwMDAw4XyG+tT4ycy8XCJKwThcdTAwMDW+uUb5h8iqXGaRXHUwMDFiJYZcIq84Z5ZX5jZneSqA5spcdTAwMDR99l/CwFxcannqXHUwMDA0RslGg1x1MDAwN5vSWYyq5IyaQXX759/LXHUwMDA3onUn13u5tf2jK7H9dH2RedW1lFDjufW0MGlshEM1MEt7M1hqrlx1MDAxZOxamk1cdTAwMGVcdTAwMDVAXHUwMDBl1d4uQpffssnD94beOzk9cFx1MDAwN1x1MDAxN487uaunwUaxnf+VOFQnXHUwMDA2s4zjXHUwMDAwfpZS8fjJzL5cIjpcdTAwMDaogN5afOBo52quXHUwMDE5UFx1MDAxOVx1MDAwM6fcXHUwMDAzJZdcdTAwMTjZmI9EXHL+Qo/x/Xs8/INIdFx1MDAwMqW8f4+HYP5fmEGVXHUwMDEzXmmlp2fQdCB7/5S/XHUwMDA0zTWWmsp7LlFcdTAwMDWkjVAouuLMeaFcdTAwMTRcYrSAhV2eXHUwMDFiupguXHUwMDBmXHUwMDAwSMNUppnVNlx1MDAwZnMp+JS5yulm68q7tHlIR4HAmH63eVxinP2TbVx1MDAxZdKT31JLSyQ3zNBcbijemspmwvFcdTAwMTb0NId9XHUwMDFl0F9GK5dcdTAwMDfiT6M4iUSy52j6XHUwMDE42lx1MDAxYU3EbiNcdTAwMDP6WVx1MDAwND3XmlNcdTAwMGb9XHUwMDE0nMpUelUh37lNguA3zO9MKXimSnVcdTAwMWJkroDPKJNXe8BZS1x1MDAwMbQlVZfMhXuL1ThcdTAwMTQ5K6nJg9dcdTAwMDJcdTAwMTBgxlROaWbJTVx1MDAxNFx1MDAxNK+T4rVcdTAwMDZcIrlzRKKYXHUwMDBmj0ZcdTAwMDQ8qsCzgtZksE3vXHUwMDEwPlx1MDAwZbbCol+ikdmNJJRcdTAwMGbWPT+DLbVHlMJQN1x1MDAxN3w+XHUwMDE3U1x1MDAxNTJcdTAwMTVcdTAwMDGk56GNkZJ0WqA7j/48Tlx1MDAxYvpcdTAwMTRRTlJMOEJMb9CDplx1MDAwZVx1MDAxM1E1WGClSnq0JVx1MDAxNUa1XHUwMDA2xqnAXHUwMDExjPEusk2loZZcdTAwMTLcUT9cdTAwMWTUSaGiKIrne3RJlEGIVVx1MDAxNCCIXHUwMDAxUYRqQJNNIF7jm+SQtWrnj1x1MDAwYqLUXHUwMDE2XHUwMDE0gSu6xEbWuUnbqUpaKYLW5z9cZkRzVCyGuIGyZ7lDylDjKEqZSEoqtFx1MDAxY2nDLqvUxFx1MDAwYiZcbjr9REX8LWA0PVxuP1x1MDAwNlnKS++8tcPqb89FjFx1MDAxZI00QbPhcfTIXHUwMDEyqPVLx1GFL1x1MDAwNt05ZFx1MDAxZcp6XHUwMDFmXHRx0LY3XHUwMDFhod3Q/rVOLFx1MDAwZkcnLZ15kbKIzT3yppo+wamxU7h/hPzV2tbDXHUwMDE3l3+qNffWz7PfYFx1MDAxYyWfgUZbgTKcglx1MDAxZJN+7NAqXHUwMDE0Slx1MDAxOapcdTAwMTI6Rd47XHUwMDFlXHUwMDA21ffOXG6mbXdcdTAwMTAs/S+2dPbLZVx1MDAwNaO9lVxcXHUwMDFj5lx1MDAwMM01ZaehvZdd33pH+cf23Vav2jzb6lxcXHUwMDFjXHUwMDE1XHUwMDA3XHUwMDBm7lx1MDAwM+gqXHUwMDE5oORcdTAwMWJ7xHGvXCK9jCXqsvdcdTAwMTatXHUwMDAwwCnxcnm6upBMJ3xhVFx1MDAxN2WyuO3b41W5tVfb01x1MDAwZuXyua9cdTAwMTc3Ls5cdTAwMDdrpfets4tcdTAwMThcdTAwMWPvmUHleKI24ntHp1xyuWR66rz8cnZUfypcdTAwMWY8XHUwMDE0d7tfz0157fr06Cz76igtQ9hBXHUwMDA31qNcdTAwMWFEUoNcdTAwMDWtXTs0KanSXFybgFx1MDAxM5bR8K9cdTAwMDOEXHUwMDE1bvxSnJ2lhX93inVeNJf7uoMm/ONRvb/dXHUwMDFiTN3j/+lo/e5CXHUwMDBmtq9aW90v7klcdTAwMWZ83r9ZbHnq0sO/zqZ0v1LktGiYPoxcdTAwMTQ/m5nXRKXRSKW1XGbrlVx1MDAxN+F6XHUwMDE5IT1cdTAwMTNOXHUwMDE5PKaHRu7yNHGu+C9SpOfSL2JTt49kwy41/DuBUjKRQ2VdYrMlMLRSPOVSzktjm8/Vnut1t292b8+PXHUwMDA2XHUwMDA3X8qrvUo786qrhGTWSbBcdTAwMWMowzFCop4zTpTmQVx1MDAwYmpwm3VcdTAwMTJcdTAwMDVcdTAwMGZcdTAwMTL1X2YrfX/iTnP1ztP9zlWrOzjcvCuft1x1MDAwZXdcdTAwMGWuXHUwMDFlfiVcdTAwMTLVidXhmtqu2llqTuMnM/OKKIHRipxF8bVSRzhUSaasUFxuf1O314xRqFRcdTAwMTJcdTAwMWRJ5X5T6OIodFx1MDAwMqG8f1x1MDAwNpWGZP60XG6k5WpcdTAwMDbTN1x1MDAxZMYym0Elyc1cdTAwMTRAXHUwMDE5Usp7XHUwMDE3bj8jhMJpd1x1MDAxNFpcdTAwMDeqL1ieXHUwMDE3uphcZipcdTAwMDFO0LFl0GfG86fSTdaVlPwpXHUwMDE3zVVcdTAwMWHPn5JizrBPOlx1MDAwNqyk5U9F4+dj+VMuro35r54/NclOQTxITPZ2qOTeXG4zvaGSK/b9xs52ZbNRvTdcdTAwMGad0/Wnq893mTdUxHB3XHUwMDAwT61gpFx1MDAxNFqFXVx1MDAwNk59bLhcdTAwMDUyY1x1MDAxY3CdtYhcdTAwMTUoimDbRSyzfSRT5deLWEE0XHQusPsmXHUwMDA3I/1cZpVcdTAwMTm717q6e3pab5TAbOZbe6dOtVxc9pVcdTAwMTV5XGK9eq8ono5KXHUwMDE5Vlag4nyaXHInhJHgMlx1MDAxZbJcdTAwMTJcYi5O0UCXYZ9kNGa1/Fx1MDAxZY5vXHUwMDE0s7LJ6afUtsgqLqdfbrOH5W/93n7u8jS/d+NXa6vVL4NC9vVRakZcdTAwMWJioFx1MDAxZm+CSbov1ElV/1x1MDAxY5SlbpbG+ayHrNCwov2zbba0cWLBYml1a3+zqT9cdTAwMGa+lVxue3fXvf1SR02rjPnGxYCb9aJ+uFpfb94+9nJr65fZVMbErCuVzIu0sVxiOlx1MDAwNtN334ifzMyrIWUqUmN88lx1MDAxMbhcbjvtfNjRhkrruXAm0NNcIlx1MDAxYottXHUwMDE2X1x1MDAxMfp6alx1MDAwMYlcdTAwMWJcdTAwMWbJgl3qYttcdTAwMDQ6yUS8KmWVXHUwMDFjrNBIXHUwMDE5Yoa+OTtcdTAwMWTfb7f7vHt+uOaql3vHj/lAKkxWNVx1MDAxN915Rlx1MDAxYt84dOMsXHUwMDBmr5Kjt8lcZsXtlKD1aJn9cJWimHjW7NlJXGb6mLt83C61esX7s44rfPe1m8PdvewwqFx1MDAwZqrkUlx1MDAxODRQwlx1MDAxZcn5sFx1MDAxYd1cdTAwMWIrp1/4jp/N7CuiZ0BtcVx1MDAxMHe0slFcbtXIsKhcdTAwMWU4XHUwMDFk1lx1MDAwNJPRssGhQkqpLLx/wGpcdTAwMTZhzTiHTmCU91x1MDAwZljZlG6NStHSuJrBXHUwMDAzTVx1MDAwN7JcZlx1MDAwN6wko91MhbTWXHUwMDAxhCjUUzjLarTtiNuC7S2zXHUwMDE5r9JcYi7oMi8naXJcdTAwMDFcdTAwMDGrudR7yoBVus26klx1MDAxY7Aywb3VXHUwMDE2XHUwMDFisErHgJWUgJWL6ULwO2I1XcV/+tJjesW/t0yQvHqvjNDhXHUwMDBlICCBlmRcdTAwMDXFrik/TEX0mDMrjaVtilx1MDAxZPV3VYE15pElTFx0oJYsc+6oXHUwMDFl1mat4H8uXHUwMDAwzkatqnRUgS7i9lmRyVx0jojwmpq6LGn3tblgb7GlqslyST9RiYwq3KwgM8V2kql1T+OILdBEdY4rLj06kipcdTAwMWHOV/iATnh0g61CXHUwMDFlhzl3k0xcdTAwMGZsjSM2UoMwXHUwMDE0gUFL20lcdTAwMGLRtlx1MDAwMZKCPJxa2Unar+BndyWfUKKfXs6TvomuNlxmuVx1MDAwNZ9cdTAwMDSE1iq8kiBccmfOXHTtXHJY6qhcdTAwMTHT6IRplHXaWlxuUIK0lTFccuFcdD01NzhcdTAwMTc+e5j3cevzh4vU0kdcdTAwMWLvXHJdveTVbUP1XHUwMDA3ik+VovExQS9ZKIeHXHJcdTAwMWF5XHUwMDA2jVx1MDAxMMVpK1x1MDAxN1x1MDAxMHriXHUwMDA1OTPWXHUwMDEzXHUwMDEwofyicVx1MDAxMr7guIBnXGZChdO0Z1x1MDAxMZq+4CyXJjD0UXU+2jRcdTAwMWG9PpwvyoWfs83V1NX5OJ9KWWGBQFtcdTAwMTDKRFx1MDAwN+WYplxufrS1LJWtgYqMaZFcdTAwMTCaXHUwMDFlXU7vcuIlk5ZePj6IkeFccnmBdoSnNlx1MDAxMMLj89qALz9K4FG0IzJcdTAwMTKcoVxyNeJ2/kTlYo52y7LokapcdTAwMDBM/8bQ11x1MDAwYs+FoZY27fXKx2GoVImBXHUwMDA2ammK+jJV4syHhFCL6lx1MDAwN5xaLtBm3oZD8Oyc8OQmK6E0SrzQcjKAWs2GXHKNuFx1MDAwM1x1MDAwNE+j/Nj1wuL9XHUwMDE2XGI6tcGHXHUwMDE2NEKiRlx1MDAwYsh71HBcdTAwMGbRNFfFPFp6XHUwMDAykYOyqWSMubdw/ERrTdNwnEBI4VxcRpcyXGbzXGKb3iPTeVx1MDAxNCqzMFx1MDAwMP3j5X18yrdaJ11cdTAwMTTf10f61KtcdTAwMTT7q1GA+Vdp+EMxjOHdXHRcXIpDVf37j7//XHUwMDBm4Vx1MDAxZEpAIn0= + + + + + QUICServerQUICConnectionQUICClientQUICStreamQUICStreamQUICSocketQUICConnectionMapQUICConnectionQUICClientQUICStreamQUICStreamQUICConnectionQUICStreamQUICStreamQUICConnectionQUICStreamQUICStream \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 58aec2df..a0c20002 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,11 @@ "license": "Apache-2.0", "dependencies": { "@matrixai/async-cancellable": "^1.1.1", - "@matrixai/async-init": "^1.8.4", + "@matrixai/async-init": "^1.10.0", "@matrixai/async-locks": "^4.0.0", "@matrixai/contexts": "^1.2.0", - "@matrixai/errors": "^1.1.7", + "@matrixai/errors": "^1.2.0", + "@matrixai/events": "^3.2.0", "@matrixai/logger": "^3.1.0", "@matrixai/resources": "^1.1.5", "@matrixai/timer": "^1.1.1", @@ -22,36 +23,38 @@ "devDependencies": { "@fast-check/jest": "^1.1.0", "@napi-rs/cli": "^2.15.2", + "@noble/ed25519": "^1.7.3", "@peculiar/asn1-pkcs8": "^2.3.0", "@peculiar/asn1-schema": "^2.3.0", "@peculiar/asn1-x509": "^2.3.0", - "@peculiar/webcrypto": "^1.4.0", + "@peculiar/webcrypto": "^1.4.3", "@peculiar/x509": "^1.8.3", - "@swc/core": "^1.3.62", - "@swc/jest": "^0.2.26", - "@types/jest": "^28.1.3", - "@types/node": "^18.15.0", - "@typescript-eslint/eslint-plugin": "^5.45.1", - "@typescript-eslint/parser": "^5.45.1", + "@swc/core": "1.3.82", + "@swc/jest": "^0.2.29", + "@types/jest": "^29.5.2", + "@types/node": "^20.5.7", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", "benny": "^3.7.1", "common-tags": "^1.8.2", - "eslint": "^8.15.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-prettier": "^4.0.0", - "jest": "^28.1.1", - "jest-extended": "^3.0.1", - "jest-junit": "^14.0.0", - "prettier": "^2.6.2", + "eslint": "^8.44.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0-alpha.2", + "fast-check": "^3.0.1", + "jest": "^29.6.2", + "jest-extended": "^4.0.0", + "jest-junit": "^16.0.0", + "prettier": "^3.0.0", "semver": "^7.3.7", "shx": "^0.3.4", "sodium-native": "^3.4.1", "systeminformation": "^5.18.5", - "ts-jest": "^28.0.5", + "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", - "typedoc": "^0.23.21", - "typescript": "^4.9.3" + "typedoc": "^0.24.8", + "typescript": "^5.1.6" }, "optionalDependencies": { "@matrixai/quic-darwin-arm64": "0.0.21", @@ -597,7 +600,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1026,97 +1028,59 @@ } }, "node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.4.tgz", + "integrity": "sha512-wNK6gC0Ha9QeEPSkeJedQuTQqxZYnDPuDcDhVuVatRvMkL4D0VTvFVZj+Yuh6caG2aOfzkUZ36KtCmLNtR02hw==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/console/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/console/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/core": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", - "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.4.tgz", + "integrity": "sha512-U/vq5ccNTSVgYH7mHnodHmCffGWHJnz/E1BEWlLuK5pM4FZmGfBn/nrJGLjUsSmyx3otCeqc1T31F4y08AMDLg==", "dev": true, "dependencies": { - "@jest/console": "^28.1.3", - "@jest/reporters": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/console": "^29.6.4", + "@jest/reporters": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.1.3", - "jest-config": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-resolve-dependencies": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "jest-watcher": "^28.1.3", + "jest-changed-files": "^29.6.3", + "jest-config": "^29.6.4", + "jest-haste-map": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-resolve-dependencies": "^29.6.4", + "jest-runner": "^29.6.4", + "jest-runtime": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "jest-watcher": "^29.6.4", "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "rimraf": "^3.0.0", + "pretty-format": "^29.6.3", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1127,153 +1091,6 @@ } } }, - "node_modules/@jest/core/node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", - "dev": true, - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-snapshot": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", - "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, "node_modules/@jest/create-cache-key-function": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz", @@ -1312,148 +1129,89 @@ } }, "node_modules/@jest/environment": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.2.tgz", - "integrity": "sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/fake-timers": "^29.6.2", - "@jest/types": "^29.6.1", - "@types/node": "*", - "jest-mock": "^29.6.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/environment/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.4.tgz", + "integrity": "sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==", "dev": true, - "peer": true, "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/fake-timers": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "jest-mock": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.2.tgz", - "integrity": "sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-Warhsa7d23+3X5bLbrbYvaehcgX5TLYhI03JKoedTiI8uJU4IhqYBWF7OSSgUyz4IgLpUYPkK0AehA5/fRclAA==", "dev": true, - "peer": true, "dependencies": { - "expect": "^29.6.2", - "jest-snapshot": "^29.6.2" + "expect": "^29.6.4", + "jest-snapshot": "^29.6.4" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.2.tgz", - "integrity": "sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.4.tgz", + "integrity": "sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==", "dev": true, - "peer": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.2.tgz", - "integrity": "sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.4.tgz", + "integrity": "sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==", "dev": true, - "peer": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.6.2", - "jest-mock": "^29.6.2", - "jest-util": "^29.6.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "jest-message-util": "^29.6.3", + "jest-mock": "^29.6.3", + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.2.tgz", - "integrity": "sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/environment": "^29.6.2", - "@jest/expect": "^29.6.2", - "@jest/types": "^29.6.1", - "jest-mock": "^29.6.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.4.tgz", + "integrity": "sha512-wVIn5bdtjlChhXAzVXavcY/3PEjf4VqM174BM3eGL5kMxLiZD5CLnbmkEyA1Dwh9q8XjP6E8RwjBsY/iCWrWsA==", "dev": true, - "peer": true, "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@jest/environment": "^29.6.4", + "@jest/expect": "^29.6.4", + "@jest/types": "^29.6.3", + "jest-mock": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", - "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.4.tgz", + "integrity": "sha512-sxUjWxm7QdchdrD3NfWKrL8FBsortZeibSJv4XLjESOOjSUOkjQcb0ZHJwfhEGIvBvTluTzfG2yZWZhkrXJu8g==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", + "@jest/console": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -1461,21 +1219,20 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1486,47 +1243,26 @@ } } }, - "node_modules/@jest/reporters/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": ">=10" } }, "node_modules/@jest/schemas": { - "version": "29.6.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", - "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -1536,99 +1272,88 @@ } }, "node_modules/@jest/source-map": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", - "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.13", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.4.tgz", + "integrity": "sha512-uQ1C0AUEN90/dsyEirgMLlouROgSY+Wc/JanVVk0OiUKa5UFh7sJpMEM3aoUBAz2BRNvUJ8j3d294WFuRxSyOQ==", "dev": true, "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/console": "^29.6.4", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", - "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.4.tgz", + "integrity": "sha512-E84M6LbpcRq3fT4ckfKs9ryVanwkaIB0Ws9bw3/yP4seRLg/VaCZ/LgW0MCq5wwk4/iP/qnilD41aj2fsw2RMg==", "dev": true, "dependencies": { - "@jest/test-result": "^28.1.3", + "@jest/test-result": "^29.6.4", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", + "jest-haste-map": "^29.6.4", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/transform": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", - "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.4.tgz", + "integrity": "sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", + "jest-haste-map": "^29.6.4", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" + "write-file-atomic": "^4.0.2" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^28.1.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -1636,27 +1361,9 @@ "chalk": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/types/node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types/node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1711,12 +1418,13 @@ "integrity": "sha512-f0yxu7dHwvffZ++7aCm2WIcCJn18uLcOTdCCwEA3R3KVHYE3TG/JNoTWD9/mqBkAV1AI5vBfJzg27WnF9rOUXQ==" }, "node_modules/@matrixai/async-init": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.8.4.tgz", - "integrity": "sha512-33cGC7kHTs9KKwMHJA5d5XURWhx3QUq7lLxPEXLoVfWdTHixcWNvtfshAOso0hbRfx1P3ZSgsb+ZHaIASHhWfg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.10.0.tgz", + "integrity": "sha512-JjUFu6rqd+dtTHFJ6z8bjbceuFGBj/APWfJByVsfbEH1DJsOgWERFcW3DBUrS0mgTph4Vl518tsNcsSwKT5Y+g==", "dependencies": { "@matrixai/async-locks": "^4.0.0", - "@matrixai/errors": "^1.1.7" + "@matrixai/errors": "^1.2.0", + "@matrixai/events": "^3.2.0" } }, "node_modules/@matrixai/async-locks": { @@ -1743,18 +1451,62 @@ } }, "node_modules/@matrixai/errors": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.1.7.tgz", - "integrity": "sha512-WD6MrlfgtNSTfXt60lbMgwasS5T7bdRgH4eYSOxV+KWngqlkEij9EoDt5LwdvcMD1yuC33DxPTnH4Xu2XV3nMw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.2.0.tgz", + "integrity": "sha512-eZHPHFla5GFmi0O0yGgbtkca+ZjwpDbMz+60NC3y+DzQq6BMoe4gHmPjDalAHTxyxv0+Q+AWJTuV8Ows+IqBfQ==", "dependencies": { "ts-custom-error": "3.2.2" } }, + "node_modules/@matrixai/events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/events/-/events-3.2.0.tgz", + "integrity": "sha512-MiUr8cUQyGAZCU7EbbMZI1xiYtLnWi/FMSCYpuV+14cMtmU4qfZpCT/nUh+xUNZS3P/LOgR/VjW56BsrJTfICw==", + "engines": { + "node": ">=19.0.0" + } + }, "node_modules/@matrixai/logger": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-3.1.0.tgz", "integrity": "sha512-C4JWpgbNik3V99bfGfDell5cH3JULD67eEq9CeXl4rYgsvanF8hhuY84ZYvndPhimt9qjA9/Z8uExKGoiv1zVw==" }, + "node_modules/@matrixai/quic-darwin-arm64": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@matrixai/quic-darwin-arm64/-/quic-darwin-arm64-0.0.21.tgz", + "integrity": "sha512-1JpL9Z2gKvGq9UG8AhjjZ9F2saEuA0MO7URcUoIHUmxiHsoRPE94n5cLDd6CdYtYdCihB9YGfmfltrpdbNWpUw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@matrixai/quic-darwin-x64": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@matrixai/quic-darwin-x64/-/quic-darwin-x64-0.0.21.tgz", + "integrity": "sha512-gBGT9BMjRSVdHfdt9y3uCyVVfwUHOErXm2k3uCRueENoaZA8KXgPktEKzirU2tysDA9hC1kc98gX0mVuODTUwQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@matrixai/quic-linux-x64": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@matrixai/quic-linux-x64/-/quic-linux-x64-0.0.21.tgz", + "integrity": "sha512-sExK7URQbv5I5bmkYozCj5K7EacfZAU9S+2oWMDWx5hpizYBXJLJLDULuF/uRz8+qBWE8Gw9g1T7F757q9lANA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@matrixai/resources": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@matrixai/resources/-/resources-1.1.5.tgz", @@ -1785,6 +1537,18 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1994,6 +1758,26 @@ "tsyringe": "^4.8.0" } }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2005,7 +1789,6 @@ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", "dev": true, - "peer": true, "dependencies": { "type-detect": "4.0.8" } @@ -2015,17 +1798,19 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/@swc/core": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.78.tgz", - "integrity": "sha512-y6DQP571v7fbUUY7nz5G4lNIRGofuO48K5pGhD9VnuOCTuptfooCdi8wnigIrIhM/M4zQ53m/YCMDCbOtDgEww==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.82.tgz", + "integrity": "sha512-jpC1a18HMH67018Ij2jh+hT7JBFu7ZKcQVfrZ8K6JuEY+kjXmbea07P9MbQUZbAe0FB+xi3CqEVCP73MebodJQ==", "dev": true, "hasInstallScript": true, + "dependencies": { + "@swc/types": "^0.1.4" + }, "engines": { "node": ">=10" }, @@ -2034,16 +1819,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.78", - "@swc/core-darwin-x64": "1.3.78", - "@swc/core-linux-arm-gnueabihf": "1.3.78", - "@swc/core-linux-arm64-gnu": "1.3.78", - "@swc/core-linux-arm64-musl": "1.3.78", - "@swc/core-linux-x64-gnu": "1.3.78", - "@swc/core-linux-x64-musl": "1.3.78", - "@swc/core-win32-arm64-msvc": "1.3.78", - "@swc/core-win32-ia32-msvc": "1.3.78", - "@swc/core-win32-x64-msvc": "1.3.78" + "@swc/core-darwin-arm64": "1.3.82", + "@swc/core-darwin-x64": "1.3.82", + "@swc/core-linux-arm-gnueabihf": "1.3.82", + "@swc/core-linux-arm64-gnu": "1.3.82", + "@swc/core-linux-arm64-musl": "1.3.82", + "@swc/core-linux-x64-gnu": "1.3.82", + "@swc/core-linux-x64-musl": "1.3.82", + "@swc/core-win32-arm64-msvc": "1.3.82", + "@swc/core-win32-ia32-msvc": "1.3.82", + "@swc/core-win32-x64-msvc": "1.3.82" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -2055,9 +1840,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.78.tgz", - "integrity": "sha512-596KRua/d5Gx1buHKKchSyHuwoIL4S1BRD/wCvYNLNZ3xOzcuBBmXOjrDVigKi1ztNDeS07p30RO5UyYur0XAA==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.82.tgz", + "integrity": "sha512-JfsyDW34gVKD3uE0OUpUqYvAD3yseEaicnFP6pB292THtLJb0IKBBnK50vV/RzEJtc1bR3g1kNfxo2PeurZTrA==", "cpu": [ "arm64" ], @@ -2071,9 +1856,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.78.tgz", - "integrity": "sha512-w0RsD1onQAj0vuLAoOVi48HgnW6D6oBEIZP17l0HYejCDBZ+FRZLjml7wgNAWMqHcd2qNRqgtZ+v7aLza2JtBQ==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.82.tgz", + "integrity": "sha512-ogQWgNMq7qTpITjcP3dnzkFNj7bh6SwMr859GvtOTrE75H7L7jDWxESfH4f8foB/LGxBKiDNmxKhitCuAsZK4A==", "cpu": [ "x64" ], @@ -2087,9 +1872,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.78.tgz", - "integrity": "sha512-v1CpRn+H6fha1WIqmdRvJM40pFdjUHrGfhf4Ygci72nlAU41l5XimN8Iwkm8FgIwf2wnv0lLzedSM4IHvpq/yA==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.82.tgz", + "integrity": "sha512-7TMXG1lXlNhD0kUiEqs+YlGV4irAdBa2quuy+XI3oJf2fBK6dQfEq4xBy65B3khrorzQS3O0oDGQ+cmdpHExHA==", "cpu": [ "arm" ], @@ -2103,9 +1888,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.78.tgz", - "integrity": "sha512-Sis17dz9joJRFVvR/gteOZSUNrrrioo81RQzani0Zr5ZZOfWLMTB9DA+0MVlfnVa2taYcsJHJZFoAv9JkLwbzg==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.82.tgz", + "integrity": "sha512-26JkOujbzcItPAmIbD5vHJxQVy5ihcSu3YHTKwope1h28sApZdtE7S3e2G3gsZRTIdsCQkXUtAQeqHxGWWR3pw==", "cpu": [ "arm64" ], @@ -2119,9 +1904,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.78.tgz", - "integrity": "sha512-E5F8/qp+QupnfBnsP4vN1PKyCmAHYHDG1GMyPE/zLFOUYLgw+jK4C9rfyLBR0o2bWo1ay2WCIjusBZD9XHGOSA==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.82.tgz", + "integrity": "sha512-8Izj9tuuMpoc3cqiPBRtwqpO1BZ/+sfZVsEhLxrbOFlcSb8LnKyMle1g3JMMUwI4EU75RGVIzZMn8A6GOKdJbA==", "cpu": [ "arm64" ], @@ -2135,9 +1920,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.78.tgz", - "integrity": "sha512-iDxa+RknnTQlyy+WfPor1FM6y44ERNI2E0xiUV6gV6uPwegCngi8LFC+E7IvP6+p+yXtAkesunAaiZ8nn0s+rw==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.82.tgz", + "integrity": "sha512-0GSrIBScQwTaPv46T2qB7XnDYxndRCpwH4HMjh6FN+I+lfPUhTSJKW8AonqrqT1TbpFIgvzQs7EnTsD7AnSCow==", "cpu": [ "x64" ], @@ -2151,9 +1936,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.78.tgz", - "integrity": "sha512-dWtIYUFL5sMTE2UKshkXTusHcK8+zAhhGzvqWq1wJS45pqTlrAbzpyqB780fle880x3A6DMitWmsAFARdNzpuQ==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.82.tgz", + "integrity": "sha512-KJUnaaepDKNzrEbwz4jv0iC3/t9x0NSoe06fnkAlhh2+NFKWKKJhVCOBTrpds8n7eylBDIXUlK34XQafjVMUdg==", "cpu": [ "x64" ], @@ -2167,9 +1952,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.78.tgz", - "integrity": "sha512-CXFaGEc2M9Su3UoUMC8AnzKb9g+GwPxXfakLWZsjwS448h6jcreExq3nwtBNdVGzQ26xqeVLMFfb1l/oK99Hwg==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.82.tgz", + "integrity": "sha512-TR3MHKhDYIyGyFcyl2d/p1ftceXcubAhX5wRSOdtOyr5+K/v3jbyCCqN7bbqO5o43wQVCwwR/drHleYyDZvg8Q==", "cpu": [ "arm64" ], @@ -2183,9 +1968,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.78.tgz", - "integrity": "sha512-FaH1jwWnJpWkdImpMoiZpMg9oy9UUyZwltzN7hFwjR48e3Li82cRFb+9PifIBHCUSBM+CrrsJXbHP213IMVAyw==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.82.tgz", + "integrity": "sha512-ZX4HzVVt6hs84YUg70UvyBJnBOIspmQQM0iXSzBvOikk3zRoN7BnDwQH4GScvevCEBuou60+i4I6d5kHLOfh8Q==", "cpu": [ "ia32" ], @@ -2199,9 +1984,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.78", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.78.tgz", - "integrity": "sha512-oYxa+tPdhlx1aH14AIoF6kvVjo49tEOW0drNqoEaVHufvgH0y43QU2Jum3b2+xXztmMRtzK2CSN3GPOAXDKKKg==", + "version": "1.3.82", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.82.tgz", + "integrity": "sha512-4mJMnex21kbQoaHeAmHnVwQN9/XAfPszJ6n9HI7SVH+aAHnbBIR0M59/b50/CJMjTj5niUGk7EwQ3nhVNOG32g==", "cpu": [ "x64" ], @@ -2230,6 +2015,12 @@ "@swc/core": "*" } }, + "node_modules/@swc/types": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.4.tgz", + "integrity": "sha512-z/G02d+59gyyUb7KYhKi9jOhicek6QD2oMaotUyG+lUkybpXoV49dY9bj7Ah5Q+y7knK2jU67UTX9FyfGzaxQg==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2329,126 +2120,13 @@ } }, "node_modules/@types/jest": { - "version": "28.1.8", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz", - "integrity": "sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==", - "dev": true, - "dependencies": { - "expect": "^28.0.0", - "pretty-format": "^28.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", + "version": "29.5.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", + "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", "dev": true, "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "expect": "^29.0.0", + "pretty-format": "^29.0.0" } }, "node_modules/@types/json-schema": { @@ -2464,15 +2142,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.5.tgz", - "integrity": "sha512-xNbS75FxH6P4UXTPUJp/zNPq6/xsfdJKussCWNOnz4aULWIRwMgP1LgaB5RiBnMX1DPCYenuqGZfnIAx5mbFLA==", - "dev": true - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "version": "20.5.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", + "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", "dev": true }, "node_modules/@types/semver": { @@ -2970,21 +2642,21 @@ } }, "node_modules/babel-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", - "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.4.tgz", + "integrity": "sha512-meLj23UlSLddj6PC+YTOFRgDAtjnZom8w/ACsrx0gtPtv5cJZk0A5Unk5bV4wixD7XaPCN1fQvpww8czkZURmw==", "dev": true, "dependencies": { - "@jest/transform": "^28.1.3", + "@jest/transform": "^29.6.4", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^28.1.3", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" @@ -3007,9 +2679,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", - "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -3018,7 +2690,7 @@ "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -3045,16 +2717,16 @@ } }, "node_modules/babel-preset-jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", - "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^28.1.3", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -3096,6 +2768,27 @@ "node": ">=12" } }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3177,6 +2870,21 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3419,10 +3127,18 @@ } }, "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } }, "node_modules/deep-is": { "version": "0.1.4", @@ -3439,6 +3155,162 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", @@ -3474,9 +3346,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3513,9 +3385,9 @@ "dev": true }, "node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "engines": { "node": ">=12" @@ -3828,21 +3700,29 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", "dev": true, "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" }, "engines": { - "node": ">=12.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" }, "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, "eslint-config-prettier": { "optional": true } @@ -4021,18 +3901,16 @@ } }, "node_modules/expect": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.2.tgz", - "integrity": "sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.4.tgz", + "integrity": "sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==", "dev": true, - "peer": true, "dependencies": { - "@jest/expect-utils": "^29.6.2", - "@types/node": "*", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2" + "@jest/expect-utils": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4219,9 +4097,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -4730,6 +4608,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4769,6 +4662,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -4911,6 +4822,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4999,21 +4937,21 @@ } }, "node_modules/jest": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", - "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", + "integrity": "sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==", "dev": true, "dependencies": { - "@jest/core": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/core": "^29.6.4", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^28.1.3" + "jest-cli": "^29.6.4" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -5025,295 +4963,74 @@ } }, "node_modules/jest-changed-files": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", - "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.6.3.tgz", + "integrity": "sha512-G5wDnElqLa4/c66ma5PG9eRjE342lIbF6SUnTJi26C3J28Fv2TVY2rOyKB9YGbSA5ogwevgmxc4j4aVjrEK6Yg==", "dev": true, "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.6.3", "p-limit": "^3.1.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz", - "integrity": "sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.4.tgz", + "integrity": "sha512-YXNrRyntVUgDfZbjXWBMPslX1mQ8MrSG0oM/Y06j9EYubODIyHWP8hMUbjbZ19M3M+zamqEur7O80HODwACoJw==", "dev": true, "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/environment": "^29.6.4", + "@jest/expect": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", + "jest-each": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-runtime": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", "p-limit": "^3.1.0", - "pretty-format": "^28.1.3", + "pretty-format": "^29.6.3", + "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", + "node_modules/jest-cli": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.4.tgz", + "integrity": "sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==", "dev": true, "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3" + "@jest/core": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "prompts": "^2.0.1", + "yargs": "^17.3.1" }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", - "dev": true, - "dependencies": { - "expect": "^28.1.3", - "jest-snapshot": "^28.1.3" + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", - "dev": true, - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/jest-circus/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/jest-circus/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-mock": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", - "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-snapshot": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", - "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-cli": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", - "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", - "dev": true, - "dependencies": { - "@jest/core": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -5324,54 +5041,37 @@ } } }, - "node_modules/jest-cli/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, "node_modules/jest-config": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", - "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.4.tgz", + "integrity": "sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^28.1.3", - "@jest/types": "^28.1.3", - "babel-jest": "^28.1.3", + "@jest/test-sequencer": "^29.6.4", + "@jest/types": "^29.6.3", + "babel-jest": "^29.6.4", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^28.1.3", - "jest-environment-node": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", + "jest-circus": "^29.6.4", + "jest-environment-node": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-runner": "^29.6.4", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^28.1.3", + "pretty-format": "^29.6.3", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@types/node": "*", @@ -5386,248 +5086,70 @@ } } }, - "node_modules/jest-config/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, "node_modules/jest-diff": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", - "integrity": "sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.4.tgz", + "integrity": "sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz", - "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.6.3.tgz", + "integrity": "sha512-2+H+GOTQBEm2+qFSQ7Ma+BvyV+waiIFxmZF5LdpBsAEjWX8QYjSCa4FrkIYtbfXUJJJnFCYrOtt6TZ+IAiTjBQ==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-each": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz", - "integrity": "sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.3.tgz", + "integrity": "sha512-KoXfJ42k8cqbkfshW7sSHcdfnv5agDdHCPA87ZBdmHP+zJstTJc0ttQaJ/x7zK6noAL76hOuTIJ6ZkQRS5dcyg==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", - "jest-util": "^28.1.3", - "pretty-format": "^28.1.3" + "jest-get-type": "^29.6.3", + "jest-util": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-each/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-each/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", - "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", - "dev": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/jest-environment-node/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/jest-environment-node/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/jest-mock": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", - "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.4.tgz", + "integrity": "sha512-i7SbpH2dEIFGNmxGCpSc2w9cA4qVD+wfvg2ZnfQ7XVrKL0NA5uDVBIiGH8SR4F0dKEv/0qI5r+aDomDf04DpEQ==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/environment": "^29.6.4", + "@jest/fake-timers": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "jest-mock": "^29.6.3", + "jest-util": "^29.6.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-extended": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.2.4.tgz", - "integrity": "sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz", + "integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==", "dev": true, "dependencies": { "jest-diff": "^29.0.0", @@ -5646,1050 +5168,278 @@ } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", - "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.4.tgz", + "integrity": "sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.6.3", + "jest-worker": "^29.6.4", "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-haste-map/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-junit": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-14.0.1.tgz", - "integrity": "sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==", - "dev": true, - "dependencies": { - "mkdirp": "^1.0.4", - "strip-ansi": "^6.0.1", - "uuid": "^8.3.2", - "xml": "^1.0.1" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/jest-leak-detector": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", - "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", - "dev": true, - "dependencies": { - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz", - "integrity": "sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==", - "dev": true, - "peer": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.6.2", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.6.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.2.tgz", - "integrity": "sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.6.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.2.tgz", - "integrity": "sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/types": "^29.6.1", - "@types/node": "*", - "jest-util": "^29.6.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz", - "integrity": "sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", - "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", - "dev": true, - "dependencies": { - "jest-regex-util": "^28.0.2", - "jest-snapshot": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", - "dev": true, - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-snapshot": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", - "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-resolve/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", - "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", - "dev": true, - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/environment": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "graceful-fs": "^4.2.9", - "jest-docblock": "^28.1.1", - "jest-environment-node": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-leak-detector": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-resolve": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-util": "^28.1.3", - "jest-watcher": "^28.1.3", - "jest-worker": "^28.1.3", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/jest-runner/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/jest-runner/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-mock": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", - "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz", - "integrity": "sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==", - "dev": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/fake-timers": "^28.1.3", - "@jest/globals": "^28.1.3", - "@jest/source-map": "^28.1.2", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", - "dev": true, - "dependencies": { - "expect": "^28.1.3", - "jest-snapshot": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", - "dev": true, - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/globals": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", - "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", - "dev": true, - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/types": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/jest-runtime/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/jest-runtime/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-mock": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", - "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*" + "walker": "^1.0.8" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/jest-runtime/node_modules/jest-snapshot": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", - "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", "dev": true, "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "jest-haste-map": "^28.1.3", - "jest-matcher-utils": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "natural-compare": "^1.4.0", - "pretty-format": "^28.1.3", - "semver": "^7.3.5" + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": ">=10.12.0" } }, - "node_modules/jest-runtime/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "node_modules/jest-leak-detector": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", + "integrity": "sha512-0kfbESIHXYdhAdpLsW7xdwmYhLf1BRu4AA118/OxFm0Ho1b2RcTmO4oF6aAMaxpxdxnJ3zve2rgwzNBD4Zbm7Q==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.2.tgz", - "integrity": "sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==", + "node_modules/jest-matcher-utils": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz", + "integrity": "sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==", "dev": true, - "peer": true, "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.6.2", - "@jest/transform": "^29.6.2", - "@jest/types": "^29.6.1", - "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.6.2", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.6.2", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.6.2", - "jest-message-util": "^29.6.2", - "jest-util": "^29.6.2", - "natural-compare": "^1.4.0", - "pretty-format": "^29.6.2", - "semver": "^7.5.3" + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/@jest/transform": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.2.tgz", - "integrity": "sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==", + "node_modules/jest-message-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.3.tgz", + "integrity": "sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==", "dev": true, - "peer": true, "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.1", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.6.2", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "pretty-format": "^29.6.3", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "stack-utils": "^2.0.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "node_modules/jest-mock": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.3.tgz", + "integrity": "sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==", "dev": true, - "peer": true, "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "jest-util": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, - "peer": true, "engines": { - "node": ">=10" + "node": ">=6" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/jest-snapshot/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "peer": true + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/jest-snapshot/node_modules/jest-haste-map": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.2.tgz", - "integrity": "sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==", + "node_modules/jest-resolve": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.4.tgz", + "integrity": "sha512-fPRq+0vcxsuGlG0O3gyoqGTAxasagOxEuyoxHeyxaZbc9QNek0AmJWSkhjlMG+mTsj+8knc/mWb3fXlRNVih7Q==", "dev": true, - "peer": true, "dependencies": { - "@jest/types": "^29.6.1", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", + "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.6.2", - "jest-worker": "^29.6.2", - "micromatch": "^4.0.4", - "walker": "^1.0.8" + "jest-haste-map": "^29.6.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.6.3", + "jest-validate": "^29.6.3", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" } }, - "node_modules/jest-snapshot/node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "node_modules/jest-resolve-dependencies": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.4.tgz", + "integrity": "sha512-7+6eAmr1ZBF3vOAJVsfLj1QdqeXG+WYhidfLHBRZqGN24MFRIiKG20ItpLw2qRAsW/D2ZUUmCNf6irUr/v6KHA==", "dev": true, - "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.6.4" + }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/jest-worker": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.2.tgz", - "integrity": "sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==", + "node_modules/jest-runner": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.4.tgz", + "integrity": "sha512-SDaLrMmtVlQYDuG0iSPYLycG8P9jLI+fRm8AF/xPKhYDB2g6xDWjXBrR5M8gEWsK6KVFlebpZ4QsrxdyIX1Jaw==", "dev": true, - "peer": true, "dependencies": { + "@jest/console": "^29.6.4", + "@jest/environment": "^29.6.4", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-util": "^29.6.2", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.6.3", + "jest-environment-node": "^29.6.4", + "jest-haste-map": "^29.6.4", + "jest-leak-detector": "^29.6.3", + "jest-message-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-runtime": "^29.6.4", + "jest-util": "^29.6.3", + "jest-watcher": "^29.6.4", + "jest-worker": "^29.6.4", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "node_modules/jest-runtime": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.4.tgz", + "integrity": "sha512-s/QxMBLvmwLdchKEjcLfwzP7h+jsHvNEtxGP5P+Fl1FMaJX2jMiIqe4rJw4tFprzCwuSvVUo9bn0uj4gNRXsbA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.4", + "@jest/fake-timers": "^29.6.4", + "@jest/globals": "^29.6.4", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-mock": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.6.4", + "jest-snapshot": "^29.6.4", + "jest-util": "^29.6.3", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/jest-snapshot": { + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.4.tgz", + "integrity": "sha512-VC1N8ED7+4uboUKGIDsbvNAZb6LakgIPgAF4RSpF13dN6YaMokfRqO+BaqK4zIh6X3JffgwbzuGqDEjHm/MrvA==", "dev": true, - "peer": true, "dependencies": { - "has-flag": "^4.0.0" + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.6.4", + "@jest/transform": "^29.6.4", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.6.4", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.6.4", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.6.4", + "jest-message-util": "^29.6.3", + "jest-util": "^29.6.3", + "natural-compare": "^1.4.0", + "pretty-format": "^29.6.3", + "semver": "^7.5.3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.2.tgz", - "integrity": "sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.3.tgz", + "integrity": "sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==", "dev": true, - "peer": true, "dependencies": { - "@jest/types": "^29.6.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -6700,39 +5450,21 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/@jest/types": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", - "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", - "dev": true, - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-validate": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz", - "integrity": "sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.3.tgz", + "integrity": "sha512-e7KWZcAIX+2W1o3cHfnqpGajdCs1jSM3DkXjGeLSNmCazv1EeI1ggTeK5wdZhF+7N+g44JI2Od3veojoaumlfg==", "dev": true, "dependencies": { - "@jest/types": "^28.1.3", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^28.0.2", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^28.1.3" + "pretty-format": "^29.6.3" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -6747,63 +5479,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-validate/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", - "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, "node_modules/jest-watcher": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", - "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.4.tgz", + "integrity": "sha512-oqUWvx6+On04ShsT00Ir9T4/FvBeEh2M9PTubgITPxDa739p4hoQweWPRGyYeaojgT0xTpZKF0Y/rSY1UgMxvQ==", "dev": true, "dependencies": { - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", + "@jest/test-result": "^29.6.4", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.3", + "emittery": "^0.13.1", + "jest-util": "^29.6.3", "string-length": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watcher/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "version": "29.6.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.4.tgz", + "integrity": "sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==", "dev": true, "dependencies": { "@types/node": "*", + "jest-util": "^29.6.3", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -7316,6 +6023,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -7551,15 +6276,15 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -7578,38 +6303,19 @@ } }, "node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.3.tgz", + "integrity": "sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==", "dev": true, "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/pretty-format/node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/pretty-format/node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true - }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -7805,9 +6511,9 @@ } }, "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, "engines": { "node": ">=10" @@ -7851,6 +6557,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8239,19 +6960,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -8264,6 +6972,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/systeminformation": { "version": "5.18.15", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.18.15.tgz", @@ -8290,22 +7014,6 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -8326,6 +7034,18 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8362,32 +7082,32 @@ } }, "node_modules/ts-jest": { - "version": "28.0.8", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", - "integrity": "sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==", + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", "dev": true, "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", - "jest-util": "^28.0.0", - "json5": "^2.2.1", + "jest-util": "^29.0.0", + "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.x", + "semver": "^7.5.3", "yargs-parser": "^21.0.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/types": "^28.0.0", - "babel-jest": "^28.0.0", - "jest": "^28.0.0", - "typescript": ">=4.3" + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { "@babel/core": { @@ -8404,23 +7124,6 @@ } } }, - "node_modules/ts-jest/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -8641,14 +7344,14 @@ } }, "node_modules/typedoc": { - "version": "0.23.28", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", - "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", + "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", "dev": true, "dependencies": { "lunr": "^2.3.9", - "marked": "^4.2.12", - "minimatch": "^7.1.3", + "marked": "^4.3.0", + "minimatch": "^9.0.0", "shiki": "^0.14.1" }, "bin": { @@ -8658,7 +7361,7 @@ "node": ">= 14.14" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -8671,31 +7374,31 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -8722,6 +7425,15 @@ "node": ">= 10.0.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/package.json b/package.json index 57f6502a..24974880 100644 --- a/package.json +++ b/package.json @@ -29,18 +29,19 @@ "prepublishOnly": "node ./scripts/prepublishOnly.js", "ts-node": "ts-node", "test": "jest", - "lint": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}'", - "lintfix": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}' --fix", + "lint": "eslint '{src,tests,scripts}/**/*.{js,ts,json}' 'benches/**/*.{js,ts}'", + "lintfix": "eslint '{src,tests,scripts}/**/*.{js,ts,json}' 'benches/**/*.{js,ts}' --fix", "lint-shell": "find ./src ./tests ./scripts -type f -regextype posix-extended -regex '.*\\.(sh)' -exec shellcheck {} +", "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", - "bench": "rimraf ./benches/results && ts-node ./benches" + "bench": "npm run prebuild -- --production && rimraf ./benches/results && ts-node ./benches" }, "dependencies": { "@matrixai/async-cancellable": "^1.1.1", - "@matrixai/async-init": "^1.8.4", + "@matrixai/async-init": "^1.10.0", "@matrixai/async-locks": "^4.0.0", "@matrixai/contexts": "^1.2.0", - "@matrixai/errors": "^1.1.7", + "@matrixai/errors": "^1.2.0", + "@matrixai/events": "^3.2.0", "@matrixai/logger": "^3.1.0", "@matrixai/resources": "^1.1.5", "@matrixai/timer": "^1.1.1", @@ -55,35 +56,37 @@ "devDependencies": { "@fast-check/jest": "^1.1.0", "@napi-rs/cli": "^2.15.2", + "@noble/ed25519": "^1.7.3", "@peculiar/asn1-pkcs8": "^2.3.0", "@peculiar/asn1-schema": "^2.3.0", "@peculiar/asn1-x509": "^2.3.0", - "@peculiar/webcrypto": "^1.4.0", + "@peculiar/webcrypto": "^1.4.3", "@peculiar/x509": "^1.8.3", - "@swc/core": "^1.3.62", - "@swc/jest": "^0.2.26", - "@types/jest": "^28.1.3", - "@types/node": "^18.15.0", - "@typescript-eslint/eslint-plugin": "^5.45.1", - "@typescript-eslint/parser": "^5.45.1", + "@swc/core": "1.3.82", + "@swc/jest": "^0.2.29", + "@types/jest": "^29.5.2", + "@types/node": "^20.5.7", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", "benny": "^3.7.1", "common-tags": "^1.8.2", - "eslint": "^8.15.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-prettier": "^4.0.0", - "jest": "^28.1.1", - "jest-extended": "^3.0.1", - "jest-junit": "^14.0.0", - "prettier": "^2.6.2", + "eslint": "^8.44.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0-alpha.2", + "fast-check": "^3.0.1", + "jest": "^29.6.2", + "jest-extended": "^4.0.0", + "jest-junit": "^16.0.0", + "prettier": "^3.0.0", "semver": "^7.3.7", "shx": "^0.3.4", "sodium-native": "^3.4.1", "systeminformation": "^5.18.5", - "ts-jest": "^28.0.5", + "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", - "typedoc": "^0.23.21", - "typescript": "^4.9.3" + "typedoc": "^0.24.8", + "typescript": "^5.1.6" } } diff --git a/pkgs.nix b/pkgs.nix index bb501409..2997ae2e 100644 --- a/pkgs.nix +++ b/pkgs.nix @@ -1,4 +1,4 @@ import ( - let rev = "f294325aed382b66c7a188482101b0f336d1d7db"; in + let rev = "ea5234e7073d5f44728c499192544a84244bf35a"; in builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz" ) diff --git a/scripts/brew-install.sh b/scripts/brew-install.sh index d7b9052f..f411eb0d 100755 --- a/scripts/brew-install.sh +++ b/scripts/brew-install.sh @@ -10,8 +10,8 @@ export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 export HOMEBREW_NO_AUTO_UPDATE=1 export HOMEBREW_NO_ANALYTICS=1 -brew install node@18 -brew link --overwrite node@18 +brew reinstall node@20 +brew link --overwrite node@20 brew install cmake brew link --overwrite cmake brew install rustup-init diff --git a/scripts/choco-install.ps1 b/scripts/choco-install.ps1 index 40809929..d6aa50c9 100755 --- a/scripts/choco-install.ps1 +++ b/scripts/choco-install.ps1 @@ -21,11 +21,11 @@ if ( $null -eq $env:ChocolateyInstall ) { New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey" -ItemType "directory" -ErrorAction:SilentlyContinue choco source add --name="cache" --source="${PSScriptRoot}\..\tmp\chocolatey" --priority=1 -# Install nodejs v18.15.0 (will use cache if exists) +# Install nodejs v20.5.1 (will use cache if exists) $nodejs = "nodejs.install" -choco install "$nodejs" --version="18.15.0" --require-checksums -y +choco install "$nodejs" --version="20.5.1" --require-checksums -y # Internalise nodejs to cache if doesn't exist -if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$nodejs\$nodejs.18.15.0.nupkg" -PathType Leaf) ) { +if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$nodejs\$nodejs.20.5.1.nupkg" -PathType Leaf) ) { Save-ChocoPackage -PackageName $nodejs } diff --git a/shell.nix b/shell.nix index 5163030b..5242fc30 100644 --- a/shell.nix +++ b/shell.nix @@ -3,7 +3,7 @@ with pkgs; mkShell { nativeBuildInputs = [ - nodejs + nodejs_20 shellcheck gitAndTools.gh rustc diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 5181ef67..0a365f78 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -1,72 +1,64 @@ import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ContextTimed, ContextTimedInput } from '@matrixai/contexts'; -import type { ClientCrypto, Host, VerifyCallback } from './types'; -import type { Config } from './native/types'; -import type QUICConnectionMap from './QUICConnectionMap'; import type { - QUICConfig, + Host, + Port, + QUICClientCrypto, + ResolveHostname, + QUICClientConfigInput, StreamCodeToReason, StreamReasonToCode, } from './types'; +import type { Config } from './native/types'; import Logger from '@matrixai/logger'; -import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy'; +import { AbstractEvent, EventAll } from '@matrixai/events'; import { running } from '@matrixai/async-init'; +import { + CreateDestroy, + destroyed, + ready, + status, +} from '@matrixai/async-init/dist/CreateDestroy'; import { timedCancellable, context } from '@matrixai/contexts/dist/decorators'; -import { quiche } from './native'; -import * as utils from './utils'; -import * as errors from './errors'; -import * as events from './events'; -import { clientDefault, minIdleTimeout } from './config'; import QUICSocket from './QUICSocket'; import QUICConnection from './QUICConnection'; import QUICConnectionId from './QUICConnectionId'; +import { quiche, ConnectionErrorCode } from './native'; +import { clientDefault, minIdleTimeout } from './config'; +import * as utils from './utils'; +import * as events from './events'; +import * as errors from './errors'; -/** - * You must provide an error handler `addEventListener('error')`. - * Otherwise, errors will just be ignored. - * - * Use the same event names. - * However, it needs to bubble up. - * And the right target needs to be used. - * - * Events: - * - clientError encapsulates: - * - socketError - * - connectionError - * - clientDestroy - * - socketStop - * - connectionStream - * - connectionStop - * - streamDestroy - */ interface QUICClient extends CreateDestroy {} -@CreateDestroy() +@CreateDestroy({ + eventDestroy: events.EventQUICClientDestroy, + eventDestroyed: events.EventQUICClientDestroyed, +}) class QUICClient extends EventTarget { - public readonly isSocketShared: boolean; - protected socket: QUICSocket; - protected logger: Logger; - protected config: Config; - protected _connection: QUICConnection; - protected connectionMap: QUICConnectionMap; - // Used to track address string for logging ONLY - protected address: string; - /** - * Creates a QUIC Client + * Creates a QUIC client. * * @param opts - * @param opts.host - peer host where `0.0.0.0` becomes `127.0.0.1` and `::` becomes `::1` - * @param opts.port + * @param opts.host - target host where wildcards are resolved to point locally. + * @param opts.port - target port * @param opts.localHost - defaults to `::` (dual-stack) * @param opts.localPort - defaults 0 - * @param opts.crypto - client only needs the ability to generate random bytes - * @param opts.config - optional config - * @param opts.socket - optional QUICSocket to use - * @param opts.resolveHostname - optional hostname resolver - * @param opts.reasonToCode - optional reason to code map - * @param opts.codeToReason - optional code to reason map - * @param opts.logger - optional logger + * @param opts.socket - optional shared QUICSocket + * @param opts.crypto - client needs to generate random bytes + * @param opts.config - defaults to `clientDefault` + * @param opts.resolveHostname - defaults to using OS DNS resolver + * @param opts.reuseAddr - reuse existing port + * @param opts.ipv6Only - force using IPv6 even when using `::` + * @param opts.reasonToCode - maps stream error reasons to stream error codes + * @param opts.codeToReason - maps stream error codes to reasons + * @param opts.logger * @param ctx + * + * @throws {errors.ErrorQUICClientCreateTimeout} - if timed out + * @throws {errors.ErrorQUICClientSocketNotRunning} - if shared socket is not running + * @throws {errors.ErrorQUICClientInvalidHost} - if local host is incompatible with target host + * @throws {errors.ErrorQUICSocket} - if socket start failed + * @throws {errors.ErrorQUICConnection} - if connection start failed */ public static createQUICClient( opts: { @@ -74,56 +66,61 @@ class QUICClient extends EventTarget { port: number; localHost?: string; localPort?: number; - crypto: { - ops: ClientCrypto; - }; - config?: Partial; - socket?: QUICSocket; - resolveHostname?: (hostname: string) => Host | PromiseLike; + crypto: QUICClientCrypto; + config?: QUICClientConfigInput; + resolveHostname?: ResolveHostname; + reuseAddr?: boolean; + ipv6Only?: boolean; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + logger?: Logger; + }, + ctx?: Partial, + ): PromiseCancellable; + public static createQUICClient( + opts: { + host: string; + port: number; + socket: QUICSocket; + crypto: QUICClientCrypto; + config?: QUICClientConfigInput; reuseAddr?: boolean; ipv6Only?: boolean; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; logger?: Logger; }, ctx?: Partial, ): PromiseCancellable; - @timedCancellable(true, minIdleTimeout, errors.ErrorQUICClientCreateTimeOut) + @timedCancellable(true, minIdleTimeout, errors.ErrorQUICClientCreateTimeout) public static async createQUICClient( { host, port, localHost = '::', localPort = 0, + socket, crypto, config = {}, - socket, resolveHostname = utils.resolveHostname, reuseAddr, ipv6Only, reasonToCode, codeToReason, - verifyCallback, logger = new Logger(`${this.name}`), }: { host: string; port: number; localHost?: string; localPort?: number; - crypto: { - ops: { - randomBytes(data: ArrayBuffer): Promise; - }; - }; - config?: Partial; socket?: QUICSocket; - resolveHostname?: (hostname: string) => Host | PromiseLike; + crypto: QUICClientCrypto; + config?: QUICClientConfigInput; + resolveHostname?: ResolveHostname; reuseAddr?: boolean; ipv6Only?: boolean; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; logger?: Logger; }, @context ctx: ContextTimed, @@ -140,23 +137,17 @@ class QUICClient extends EventTarget { await crypto.ops.randomBytes(scidBuffer); const scid = new QUICConnectionId(scidBuffer); // Validating host and port types - let [host_] = await utils.resolveHost(host, resolveHostname); - const [localHost_] = await utils.resolveHost(localHost, resolveHostname); + let [host_, udpType] = await utils.resolveHost(host, resolveHostname); const port_ = utils.toPort(port); - const localPort_ = utils.toPort(localPort); // If the target host is in fact a zero IP, it cannot be used // as a target host, so we need to resolve it to a non-zero IP // in this case, 0.0.0.0 is resolved to 127.0.0.1 and :: and ::0 is - // resolved to ::1 + // resolved to ::1. host_ = utils.resolvesZeroIP(host_); - // This error promise is only used during `connection.start()`. - const { p: socketErrorP, rejectP: rejectSocketErrorP } = - utils.promise(); - const handleQUICSocketError = (e: events.QUICSocketErrorEvent) => { - rejectSocketErrorP(e.detail); - }; let isSocketShared: boolean; if (socket == null) { + const [localHost_] = await utils.resolveHost(localHost, resolveHostname); + const localPort_ = utils.toPort(localPort); socket = new QUICSocket({ resolveHostname, logger: logger.getChild(QUICSocket.name), @@ -169,52 +160,34 @@ class QUICClient extends EventTarget { ipv6Only, }); } else { + isSocketShared = true; + // If the socket is shared, it must already be started if (!socket[running]) { - throw new errors.ErrorQUICClientSocketNotRunning(); + throw new errors.ErrorQUICServerSocketNotRunning(); } - isSocketShared = true; } - socket.addEventListener('socketError', handleQUICSocketError, { - once: true, - }); - // Check that the target `host` is compatible with the bound socket host - if ( - socket.type === 'ipv4' && - !utils.isIPv4(host_) && - !utils.isIPv4MappedIPv6(host_) - ) { - throw new errors.ErrorQUICClientInvalidHost( - `Cannot connect to ${host_} on an IPv4 QUICClient`, - ); - } else if ( - socket.type === 'ipv6' && - (!utils.isIPv6(host_) || utils.isIPv4MappedIPv6(host_)) - ) { - throw new errors.ErrorQUICClientInvalidHost( - `Cannot connect to ${host_} on an IPv6 QUICClient`, - ); - } else if (socket.type === 'ipv4&ipv6' && !utils.isIPv6(host_)) { - throw new errors.ErrorQUICClientInvalidHost( - `Cannot send to ${host_} on a dual stack QUICClient`, - ); - } else if ( - socket.type === 'ipv4' && - utils.isIPv4MappedIPv6(socket.host) && - !utils.isIPv4MappedIPv6(host_) - ) { - throw new errors.ErrorQUICClientInvalidHost( - `Cannot connect to ${host_} an IPv4 mapped IPv6 QUICClient`, + try { + // Check that the target `host` is compatible with the bound socket host + // Also transform it if need be + host_ = utils.validateTarget( + socket.host, + socket.type, + host_, + udpType, + errors.ErrorQUICClientInvalidHost, ); + } catch (e) { + if (!isSocketShared) { + await socket.stop({ force: true }); + } + throw e; } - const abortController = new AbortController(); - const abortHandler = () => { - abortController.abort(ctx.signal.reason); - }; - ctx.signal.addEventListener('abort', abortHandler); - const connectionProm = QUICConnection.createQUICConnection( - { + let connection: QUICConnection; + try { + connection = new QUICConnection({ type: 'client', scid, + serverName: host, socket, remoteInfo: { host: host_, @@ -223,233 +196,432 @@ class QUICClient extends EventTarget { config: quicConfig, reasonToCode, codeToReason, - verifyCallback, - logger: logger.getChild( - `${QUICConnection.name} ${scid.toString().slice(32)}`, - ), - }, - ctx, - ); - try { - await Promise.race([connectionProm, socketErrorP]); + logger: logger.getChild(`${QUICConnection.name} ${scid.toString()}`), + }); } catch (e) { - // In case the `connection.start` is ongoing, we need to abort it - abortController.abort(e); if (!isSocketShared) { - // Stop is idempotent - await socket.stop(); + await socket.stop({ force: true }); } throw e; - } finally { - socket.removeEventListener('socketError', handleQUICSocketError); - ctx.signal.removeEventListener('abort', abortHandler); } - const connection = await connectionProm; - address = utils.buildAddress(host_, port); const client = new this({ socket, connection, isSocketShared, - address, logger, }); + if (!isSocketShared) { + socket.addEventListener(EventAll.name, client.handleEventQUICSocket); + } + socket.addEventListener( + events.EventQUICSocketStopped.name, + client.handleEventQUICSocketStopped, + { once: true }, + ); + connection.addEventListener( + EventAll.name, + client.handleEventQUICConnection, + ); + connection.addEventListener( + events.EventQUICConnectionError.name, + client.handleEventQUICConnectionError, + ); + connection.addEventListener( + events.EventQUICConnectionSend.name, + client.handleEventQUICConnectionSend, + ); + connection.addEventListener( + events.EventQUICConnectionStopped.name, + client.handleEventQUICConnectionStopped, + { once: true }, + ); + client.addEventListener( + events.EventQUICClientError.name, + client.handleEventQUICClientError, + ); + client.addEventListener( + events.EventQUICClientClose.name, + client.handleEventQUICClientClose, + { once: true }, + ); + // We have to start the connection after associating the event listeners on + // the client, because the client bridges the push flow from the connection + // to the socket. + socket.connectionMap.set(connection.connectionId, connection); + try { + await connection.start(undefined, ctx); + } catch (e) { + socket.connectionMap.delete(connection.connectionId); + socket.removeEventListener( + events.EventQUICSocketStopped.name, + client.handleEventQUICSocketStopped, + ); + if (!isSocketShared) { + socket.removeEventListener(EventAll.name, client.handleEventQUICSocket); + await socket.stop({ force: true }); + } + connection.removeEventListener( + EventAll.name, + client.handleEventQUICConnection, + ); + connection.removeEventListener( + events.EventQUICConnectionError.name, + client.handleEventQUICConnectionError, + ); + connection.removeEventListener( + events.EventQUICConnectionSend.name, + client.handleEventQUICConnectionSend, + ); + connection.removeEventListener( + events.EventQUICConnectionStopped.name, + client.handleEventQUICConnectionStopped, + ); + client.removeEventListener( + events.EventQUICClientError.name, + client.handleEventQUICClientError, + ); + client.removeEventListener( + events.EventQUICClientClose.name, + client.handleEventQUICClientClose, + ); + throw e; + } + address = utils.buildAddress(host_, port); logger.info(`Created ${this.name} to ${address}`); return client; } + public readonly isSocketShared: boolean; + public readonly connection: QUICConnection; + public readonly closedP: Promise; + + protected logger: Logger; + protected socket: QUICSocket; + protected config: Config; + protected _closed: boolean = false; + protected resolveClosedP: () => void; + + /** + * Handles `EventQUICClientError`. + * + * This event propagates all errors from `QUICClient` and `QUICConnection`. + * This means you can expect that `QUICConnection` errors will be logged + * twice. + * + * Internal errors will be thrown upwards to become an uncaught exception. + * + * @throws {errors.ErrorQUICClientInternal} + * @throws {errors.ErrorQUICConnectionInternal} + */ + protected handleEventQUICClientError = (evt: events.EventQUICClientError) => { + const error = evt.detail; + if ( + (error instanceof errors.ErrorQUICConnectionLocal || + error instanceof errors.ErrorQUICConnectionPeer) && + ((!error.data.isApp && + error.data.errorCode === ConnectionErrorCode.NoError) || + (error.data.isApp && error.data.errorCode === 0)) + ) { + // Log out the excpetion as an info when it is graceful + this.logger.info(utils.formatError(error)); + } else { + // Log out the exception as an error when it is not graceful + this.logger.error(utils.formatError(error)); + } + if ( + error instanceof errors.ErrorQUICClientInternal || + error instanceof errors.ErrorQUICConnectionInternal + ) { + throw error; + } + this.dispatchEvent( + new events.EventQUICClientClose({ + detail: error, + }), + ); + }; + /** - * This must not throw any exceptions. + * Handles `EventQUICClientClose`. + * Registered once. + * + * This event propagates errors minus the internal errors. + * All QUIC connections always close with an error, even if it is a graceful. + * + * If this event is dispatched first before `QUICClient.destroy`, it represents + * an evented close. This could originate from the `QUICSocket` or + * `QUICConnection`. If it was from the `QUICSocket`, then here it will stop + * the `QUICConnection` with an transport code `InternalError`. If it was + * from `QUICConnection`, then the `QUICConnection` will already be closing. + * Therefore attempting to stop the `QUICConnection` will be idempotent. */ - protected handleQUICSocketEvents = async (event: events.QUICSocketEvent) => { - if (event instanceof events.QUICSocketErrorEvent) { - // QUIC socket errors are re-emitted but a destroy takes place - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: new errors.ErrorQUICClient('Socket error', { - cause: event.detail, - }), - }), - ); - try { - // Force destroy means don't destroy gracefully - await this.destroy({ - force: true, - }); - } catch (e) { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: e.detail, - }), - ); - } - } else if (event instanceof events.QUICSocketStopEvent) { - // If a QUIC socket stopped, we immediately destroy - // However, the stop will have its own constraints - try { - // Force destroy means don't destroy gracefully - await this.destroy({ - force: true, - }); - } catch (e) { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: e.detail, - }), + protected handleEventQUICClientClose = async ( + evt: events.EventQUICClientClose, + ) => { + const error = evt.detail; + // Remove the error listener as we intend to stop the connection + this.connection.removeEventListener( + events.EventQUICConnectionError.name, + this.handleEventQUICConnectionError, + ); + await this.connection.stop({ + isApp: false, + errorCode: ConnectionErrorCode.InternalError, + reason: Buffer.from(error.description), + force: true, + }); + if (!(error instanceof errors.ErrorQUICClientSocketNotRunning)) { + // Only stop the socket if it was encapsulated + if (!this.isSocketShared) { + // Remove the stopped listener, as we intend to stop the socket + this.socket.removeEventListener( + events.EventQUICSocketStopped.name, + this.handleEventQUICSocketStopped, ); + try { + // Force stop of the socket even if it had a connection map + // This is because we will be stopping this `QUICClient` which + // which will stop all the relevant connections + await this.socket.stop({ force: true }); + } catch (e) { + const e_ = new errors.ErrorQUICClientInternal( + 'Failed to stop QUICSocket', + { cause: e }, + ); + this.dispatchEvent(new events.EventQUICClientError({ detail: e_ })); + } } - } else { - this.dispatchEvent(event); } + this._closed = true; + this.resolveClosedP(); + if (!this[destroyed] && this[status] !== 'destroying') { + await this.destroy({ force: true }); + } + }; + + /** + * Handles all `EventQUICSocket` events. + * Registered only if the socket is encapsulated. + */ + protected handleEventQUICSocket = (evt: EventAll) => { + if (evt.detail instanceof AbstractEvent) { + this.dispatchEvent(evt.detail.clone()); + } + }; + + /** + * Handles `EventQUICSocketStopped`. + * Registered once. + * + * It is an error if the socket was stopped while `QUICClient` wasn't + * destroyed. + */ + protected handleEventQUICSocketStopped = () => { + const e = new errors.ErrorQUICClientSocketNotRunning(); + this.removeEventListener(EventAll.name, this.handleEventQUICSocket); + this.dispatchEvent( + new events.EventQUICClientError({ + detail: e, + }), + ); }; /** - * This must not throw any exceptions. + * Handles all `EventQUICConnection` events. */ - protected handleQUICConnectionEvents = async ( - event: events.QUICConnectionEvent, + protected handleEventQUICConnection = (evt: EventAll) => { + if (evt.detail instanceof AbstractEvent) { + this.dispatchEvent(evt.detail.clone()); + } + }; + + /** + * Handles `EventQUICConnectionError`. + * + * All connection errors are redispatched as client errors. + */ + protected handleEventQUICConnectionError = ( + evt: events.EventQUICConnectionError, ) => { - if (event instanceof events.QUICConnectionErrorEvent) { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: new errors.ErrorQUICClient('Connection error', { - cause: event.detail, - }), - }), + const error = evt.detail; + this.dispatchEvent(new events.EventQUICClientError({ detail: error })); + }; + + /** + * Handles `EventQUICConnectionSend`. + * + * This will propagate the connection send buffers to the socket. + * This may be concurrent and multiple send events may be processed + * at a time. + */ + protected handleEventQUICConnectionSend = async ( + evt: events.EventQUICConnectionSend, + ) => { + try { + if (!(this.socket[running] && this.socket[status] !== 'stopping')) return; + // Uses the raw send method as the port and address is fully resolved + // and determined by `QUICConnection`. + await this.socket.send_( + evt.detail.msg, + evt.detail.port, + evt.detail.address, ); - try { - // Force destroy means don't destroy gracefully - await this.destroy({ - force: true, - }); - } catch (e) { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: e.detail, - }), - ); - } - } else if (event instanceof events.QUICConnectionStopEvent) { - try { - // Force destroy means don't destroy gracefully - await this.destroy({ - force: true, - }); - } catch (e) { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: e.detail, - }), - ); - } - } else if (event instanceof events.QUICConnectionStreamEvent) { - this.dispatchEvent( - new events.QUICConnectionStreamEvent({ detail: event.detail }), + } catch (e) { + const e_ = new errors.ErrorQUICClientInternal( + 'Failed to send data on the QUICSocket', + { + data: evt.detail, + cause: e, + }, ); - } else if (event instanceof events.QUICStreamDestroyEvent) { - this.dispatchEvent(new events.QUICStreamDestroyEvent()); - } else { - utils.never(); + this.dispatchEvent(new events.EventQUICClientError({ detail: e_ })); } }; + /** + * Handles `EventQUICConnectionStopped`. + * Registered once. + */ + protected handleEventQUICConnectionStopped = ( + evt: events.EventQUICConnectionStopped, + ) => { + const quicConnection = evt.target as QUICConnection; + quicConnection.removeEventListener( + events.EventQUICConnectionError.name, + this.handleEventQUICConnectionError, + ); + quicConnection.removeEventListener( + events.EventQUICConnectionSend.name, + this.handleEventQUICConnectionSend, + ); + quicConnection.removeEventListener( + EventAll.name, + this.handleEventQUICConnection, + ); + this.socket.connectionMap.delete(quicConnection.connectionId); + }; + public constructor({ socket, isSocketShared, connection, - address, logger, }: { socket: QUICSocket; isSocketShared: boolean; connection: QUICConnection; - address: string; logger: Logger; }) { super(); this.logger = logger; this.socket = socket; this.isSocketShared = isSocketShared; - this._connection = connection; - this.address = address; - // Listen on all socket events - socket.addEventListener('socketError', this.handleQUICSocketEvents); - socket.addEventListener('socketStop', this.handleQUICSocketEvents); - // Listen on all connection events - connection.addEventListener( - 'connectionStream', - this.handleQUICConnectionEvents, - ); - connection.addEventListener( - 'connectionStop', - this.handleQUICConnectionEvents, - ); - connection.addEventListener( - 'connectionError', - this.handleQUICConnectionEvents, - ); - connection.addEventListener( - 'streamDestroy', - this.handleQUICConnectionEvents, - ); + this.connection = connection; + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); + this.closedP = closedP; + this.resolveClosedP = resolveClosedP; } @ready(new errors.ErrorQUICClientDestroyed()) - public get host(): string { + public get host(): Host { + return this.connection.remoteHost; + } + + @ready(new errors.ErrorQUICClientDestroyed()) + public get port(): Port { + return this.connection.remotePort; + } + + @ready(new errors.ErrorQUICClientDestroyed()) + public get localHost(): Host { return this.socket.host; } @ready(new errors.ErrorQUICClientDestroyed()) - public get port(): number { + public get localPort(): Port { return this.socket.port; } - @ready(new errors.ErrorQUICClientDestroyed()) - public get connection() { - return this._connection; + public get closed() { + return this._closed; } /** - * Force destroy means that we don't destroy gracefully. - * This should only occur when an error occurs from the socket - * or from the connection. If the socket is stopped or the connection - * is stopped, then we also force destroy. - * Suppose the socket failed, and we attempt to stop the connection. - * The connection may attempt to stop gracefully. That would result in - * an exception because the socket send method no longer works. + * Destroy the QUICClient. + * + * @param opts + * @param opts.isApp - whether the destroy is initiated by the application + * @param opts.errorCode - the error code to send to the peer + * @param opts.reason - the reason to send to the peer + * @param opts.force - force controls whether to cancel streams or wait for + * streams to close gracefully */ public async destroy({ - force = false, - }: { - force?: boolean; - } = {}) { - const address = this.address; - this.logger.info(`Destroy ${this.constructor.name} on ${address}`); - // Listen on all socket events - this.socket.removeEventListener('socketError', this.handleQUICSocketEvents); - this.socket.removeEventListener('socketStop', this.handleQUICSocketEvents); - // Listen on all connection events - this._connection.removeEventListener( - 'connectionStream', - this.handleQUICConnectionEvents, + isApp = true, + errorCode = 0, + reason = new Uint8Array(), + force = true, + }: + | { + isApp: false; + errorCode?: ConnectionErrorCode; + reason?: Uint8Array; + force?: boolean; + } + | { + isApp?: true; + errorCode?: number; + reason?: Uint8Array; + force?: boolean; + } = {}) { + let address: string | undefined; + if (this.connection[running]) { + address = utils.buildAddress( + this.connection.remoteHost, + this.connection.remotePort, + ); + } + this.logger.info( + `Destroy ${this.constructor.name}${ + address != null ? ` to ${address}` : '' + }`, ); - this._connection.removeEventListener( - 'connectionStop', - this.handleQUICConnectionEvents, + if (!this._closed) { + await this.connection.stop({ + isApp, + errorCode, + reason, + force, + }); + } + await this.closedP; + this.removeEventListener( + events.EventQUICClientError.name, + this.handleEventQUICClientError, ); - this._connection.removeEventListener( - 'connectionError', - this.handleQUICConnectionEvents, + this.removeEventListener( + events.EventQUICClientClose.name, + this.handleEventQUICClientClose, ); - this._connection.removeEventListener( - 'streamDestroy', - this.handleQUICConnectionEvents, + // The socket may not have been stopped if it is shared + // In which case we just remove our listener here + this.socket.removeEventListener( + events.EventQUICSocketStopped.name, + this.handleEventQUICSocketStopped, ); - await this._connection.stop({ force }); if (!this.isSocketShared) { - await this.socket.stop({ force }); + this.socket.removeEventListener( + EventAll.name, + this.handleEventQUICSocket, + ); } - this.dispatchEvent(new events.QUICClientDestroyEvent()); - this.logger.info(`Destroyed ${this.constructor.name} on ${address}`); + // Connection listeners do not need to be removed + // Because it is handled by `this.handleEventQUICConnectionStopped`. + this.logger.info( + `Destroyed ${this.constructor.name}${ + address != null ? ` to ${address}` : '' + }`, + ); } } diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index febd2462..76a1fc67 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -5,51 +5,40 @@ import type QUICConnectionId from './QUICConnectionId'; import type { Host, Port, - QUICConfig, RemoteInfo, - StreamCodeToReason, + QUICConfig, + ConnectionMetadata, StreamId, + StreamCodeToReason, StreamReasonToCode, - VerifyCallback, } from './types'; -import type { Connection, ConnectionErrorCode, SendInfo } from './native/types'; -import type { Monitor } from '@matrixai/async-locks'; -import { Lock, LockBox, RWLockWriter } from '@matrixai/async-locks'; +import type { Connection, ConnectionError, SendInfo } from './native/types'; +import Logger from '@matrixai/logger'; +import { Timer } from '@matrixai/timer'; +import { Lock } from '@matrixai/async-locks'; +import { AbstractEvent, EventAll } from '@matrixai/events'; import { + StartStop, ready, running, - StartStop, status, } from '@matrixai/async-init/dist/StartStop'; -import Logger from '@matrixai/logger'; -import { Timer } from '@matrixai/timer'; -import { context, timedCancellable } from '@matrixai/contexts/dist/decorators'; -import { withF } from '@matrixai/resources'; -import { utils as contextsUtils } from '@matrixai/contexts'; +import { timedCancellable, context } from '@matrixai/contexts/dist/decorators'; import { buildQuicheConfig, minIdleTimeout } from './config'; import QUICStream from './QUICStream'; -import { quiche } from './native'; -import * as events from './events'; +import { quiche, ConnectionErrorCode } from './native'; import * as utils from './utils'; -import { never, promise } from './utils'; +import * as events from './events'; import * as errors from './errors'; -const timerCleanupReasonSymbol = Symbol('timerCleanupReasonSymbol'); - -/** - * Think of this as equivalent to `net.Socket`. - * Errors here are emitted to the connection only. - * Not to the server. - * - * Events (events are executed post-facto): - * - connectionStream - * - connectionStop - * - connectionError - can occur due to a timeout too - * - streamDestroy - */ interface QUICConnection extends StartStop {} -@StartStop() -class QUICConnection extends EventTarget { +@StartStop({ + eventStart: events.EventQUICConnectionStart, + eventStarted: events.EventQUICConnectionStarted, + eventStop: events.EventQUICConnectionStop, + eventStopped: events.EventQUICConnectionStopped, +}) +class QUICConnection { /** * This determines when it is a client or server connection. */ @@ -60,6 +49,11 @@ class QUICConnection extends EventTarget { */ public readonly connectionId: QUICConnectionId; + /** + * Resolves once the connection has closed. + */ + public readonly closedP: Promise; + /** * Internal native connection object. * @internal @@ -68,39 +62,22 @@ class QUICConnection extends EventTarget { /** * Internal stream map. - * This is also used by `QUICStream`. * @internal */ public readonly streamMap: Map = new Map(); - /** - * Logger. - */ protected logger: Logger; - - /** - * Underlying socket. - */ protected socket: QUICSocket; - protected config: QUICConfig; - - /** - * Converts reason to code. - * Used during `QUICStream` creation. - */ protected reasonToCode: StreamReasonToCode; - - /** - * Converts code to reason. - * Used during `QUICStream` creation. - */ protected codeToReason: StreamCodeToReason; /** - * Stream ID increment lock. + * This ensures that `recv` is serialised. + * Prevents a new `recv` call intercepting the previous `recv` call that is + * still finishing up with a `send` call. */ - protected streamIdLock: Lock = new Lock(); + protected recvLock: Lock = new Lock(); /** * Client initiated bidirectional stream starts at 0. @@ -117,260 +94,260 @@ class QUICConnection extends EventTarget { /** * Client initiated unidirectional stream starts at 2. * Increment by 4 to get the next ID. - * Currently unsupported. */ - protected _streamIdClientUni: StreamId = 0b10 as StreamId; + protected streamIdClientUni: StreamId = 0b10 as StreamId; /** * Server initiated unidirectional stream starts at 3. * Increment by 4 to get the next ID. - * Currently unsupported. */ - protected _streamIdServerUni: StreamId = 0b11 as StreamId; + protected streamIdServerUni: StreamId = 0b11 as StreamId; /** - * Internal conn timer. This is used to tick the state transitions on the - * connection. + * Quiche connection timer. This performs time delayed state transitions. */ - protected connTimeOutTimer?: Timer; + protected connTimeoutTimer?: Timer; /** * Keep alive timer. - * If the max idle time is set to >0, the connection can time out on idleness. - * Idleness is where there is no response from the other side. This can happen - * from the beginning to the establishment of the connection and while the - * connection is established. Normally there is nothing that will keep the - * connection alive if there is no activity. This keep alive mechanism will - * trigger ping frames to ensure that there is connection activity. - * If the max idle time is set to 0, the connection never times out on idleness. - * However, this keep alive mechanism will continue to work in case you need - * activity on the connection for some reason. - * Note that the timer used for the `ContextTimed` in `QUICClient.createQUICClient` - * is independent of the max idle time. This keep alive mechanism will only - * start working after secure establishment. + * + * Quiche does not natively ensure activity on the connection. This interval + * timer guarantees that there will be activity on the connection by sending + * acknowlegement eliciting frames, which will cause the peer to acknowledge. + * + * This is still useful even if the `config.maxIdleTimeout` is set to 0, which + * means the connection will never timeout due to being idle. + * + * This mechanism will only start working after `secureEstablishedP`. */ protected keepAliveIntervalTimer?: Timer; /** - * This can change on every `recv` call + * Remote host which can change on every `QUICConnection.recv`. */ protected _remoteHost: Host; /** - * This can change on every `recv` call + * Remote port which can change on every `QUICConnection.recv`. */ protected _remotePort: Port; /** - * Bubble up stream destroy event + * Chain of local certificates from leaf to root in DER format. */ - protected handleQUICStreamDestroyEvent = () => { - this.dispatchEvent(new events.QUICStreamDestroyEvent()); - }; + protected certDERs: Array = []; /** - * Connection establishment. - * This can resolve or reject. - * Rejections cascade down to `secureEstablishedP` and `closedP`. + * Array of independent CA certificates in DER format. */ - protected establishedP: Promise; + protected caDERs: Array = []; /** - * Connection has been verified and secured. - * This can only happen after `establishedP`. - * On the server side, being established means it is also secure established. - * On the client side, after being established, the client must wait for the - * first short frame before it is also secure established. - * This can resolve or reject. - * Rejections cascade down to `closedP`. + * Secure connection establishment means that this connection has completed + * peer certificate verification, and the connection is ready to be used. */ - protected secureEstablishedP: Promise; + protected secureEstablished = false; /** - * Connection closed promise. - * This can resolve or reject. + * Resolves after connection is established and peer certs have been verified. */ - protected closedP: Promise; - - protected resolveEstablishedP: () => void; - protected rejectEstablishedP: (reason?: any) => void; + protected secureEstablishedP: Promise; protected resolveSecureEstablishedP: () => void; protected rejectSecureEstablishedP: (reason?: any) => void; + protected resolveClosedP: () => void; - protected rejectClosedP: (reason?: any) => void; - public readonly lockbox = new LockBox(); - public readonly lockCode = 'ConnectionEventLockId'; + /** + * Stores the last dispatched error. If no error, it will be `null`. + * + * `QUICConnection.stop` will need this if it is called by the user concurrent + * with handling `EventQUICConnectionError`. It ensures that `this.stop` uses + * the original error when force destroying the streams. + */ + protected errorLast: + | errors.ErrorQUICConnectionLocal + | errors.ErrorQUICConnectionPeer + | errors.ErrorQUICConnectionIdleTimeout + | errors.ErrorQUICConnectionInternal + | null = null; - protected customVerified = false; - protected shortReceived = false; - protected secured = false; - protected count = 0; - protected verifyCallback: VerifyCallback | undefined; + /** + * Handle `EventQUICConnectionError`. + * + * QUIC connections always close with errors. Graceful errors are logged out + * at INFO level, while non-graceful errors are logged out at ERROR level. + * + * Internal errors will be thrown upwards to become an uncaught exception. + * + * @throws {errors.ErrorQUICConnectionInternal} + */ + protected handleEventQUICConnectionError = ( + evt: events.EventQUICConnectionError, + ) => { + const error = evt.detail; + this.errorLast = error; + if ( + (error instanceof errors.ErrorQUICConnectionLocal || + error instanceof errors.ErrorQUICConnectionPeer) && + ((!error.data.isApp && + error.data.errorCode === ConnectionErrorCode.NoError) || + (error.data.isApp && error.data.errorCode === 0)) + ) { + // Log out the excpetion as an info when it is graceful + this.logger.info(utils.formatError(error)); + } else { + // Log out the exception as an error when it is not graceful + this.logger.error(utils.formatError(error)); + } + if (error instanceof errors.ErrorQUICConnectionInternal) { + throw error; + } + this.dispatchEvent( + new events.EventQUICConnectionClose({ + detail: error, + }), + ); + }; - public static createQUICConnection( - args: - | { - type: 'client'; - scid: QUICConnectionId; - dcid?: undefined; - remoteInfo: RemoteInfo; - config: QUICConfig; - socket: QUICSocket; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; - logger?: Logger; - } - | { - type: 'server'; - scid: QUICConnectionId; - dcid: QUICConnectionId; - remoteInfo: RemoteInfo; - data: Uint8Array; - config: QUICConfig; - socket: QUICSocket; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; - logger?: Logger; - }, - ctx?: Partial, - ): PromiseCancellable; - @timedCancellable( - true, - minIdleTimeout, - errors.ErrorQUICConnectionStartTimeOut, - ) - public static async createQUICConnection( - args: - | { - type: 'client'; - scid: QUICConnectionId; - dcid?: undefined; - remoteInfo: RemoteInfo; - config: QUICConfig; - socket: QUICSocket; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; - logger?: Logger; - } - | { - type: 'server'; - scid: QUICConnectionId; - dcid: QUICConnectionId; - remoteInfo: RemoteInfo; - data: Uint8Array; - config: QUICConfig; - socket: QUICSocket; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; - logger?: Logger; - }, - @context ctx: ContextTimed, - ): Promise { - ctx.signal.throwIfAborted(); - const abortProm = promise(); - const abortHandler = () => { - abortProm.rejectP(ctx.signal.reason); - }; - ctx.signal.addEventListener('abort', abortHandler); - const connection = new this(args); - // If it's a server connection we want to pass the initial packet - const data = args.type === 'server' ? args.data : undefined; - // This ensures that TLS has been established and verified on both sides - try { - await Promise.race([ - Promise.all([ - connection.start(data), - connection.establishedP, - connection.secureEstablishedP, - ]), - abortProm.p, - ]); - } catch (e) { - const code = - args.reasonToCode != null ? (await args.reasonToCode(e)) ?? 0 : 0; - await connection.stop({ - applicationError: false, - errorCode: code, - errorMessage: e.message, + /** + * Handles `EventQUICConnectionClose`. + * Registered once. + * + * This event means that `this.conn.close()` is already called. + * It does not mean that `this.closedP` is resolved. The resolving of + * `this.closedP` depends on the `this.connTimeoutTimer`. + */ + protected handleEventQUICConnectionClose = async ( + evt: events.EventQUICConnectionClose, + ) => { + const error = evt.detail; + // Reject the secure established promise + // This will allow `this.start()` to reject with the error. + // No effect if this connection is running. + if (!this.secureEstablished) { + this.rejectSecureEstablishedP(error); + } + // Complete the final send call if it was a local close + if (error instanceof errors.ErrorQUICConnectionLocal) { + await this.send(); + } + // If the connection is still running, we will force close the connection + if (this[running] && this[status] !== 'stopping') { + // The `stop` will need to retrieve the error from `this.errorLast` + await this.stop({ force: true, }); - throw e; - } finally { - ctx.signal.removeEventListener('abort', abortHandler); } - connection.logger.debug('secureEstablishedP'); - // After this is done - // We need to establish the keep alive interval time - if (connection.config.keepAliveIntervalTime != null) { - connection.startKeepAliveIntervalTimer( - connection.config.keepAliveIntervalTime, - ); + }; + + /** + * Handles all `EventQUICStream` events. + */ + protected handleEventQUICStream = (evt: EventAll) => { + if (evt.detail instanceof AbstractEvent) { + this.dispatchEvent(evt.detail.clone()); } + }; - return connection; - } + /** + * Handles `EventQUICStreamSend`. + * + * This handler will trigger `this.send` when there is data in the writable + * stream buffer. As this is asynchronous, it is necessary to check if this + * `QUICConnection` is still running. + */ + protected handleEventQUICStreamSend = async () => { + if (this[running]) await this.send(); + }; + /** + * Handles `EventQUICStreamDestroyed`. + * Registered once. + */ + protected handleEventQUICStreamDestroyed = ( + evt: events.EventQUICStreamDestroyed, + ) => { + const quicStream = evt.target as QUICStream; + quicStream.removeEventListener( + events.EventQUICStreamSend.name, + this.handleEventQUICStreamSend, + ); + quicStream.removeEventListener(EventAll.name, this.handleEventQUICStream); + this.streamMap.delete(quicStream.streamId); + }; + + /** + * Constructs a QUIC connection. + * + * @param opts + * @param opts.type - client or server connection + * @param opts.scid - source connection ID + * @param opts.dcid - destination connection ID + * @param opts.serverName - client connections can use server name for + * verifying the server's certificate, however if + * `config.verifyCallback` is set, this will have no + * effect + * @param opts.remoteInfo - remote host and port + * @param opts.config - QUIC config + * @param opts.socket - injected socket + * @param opts.reasonToCode - maps stream error reasons to stream error codes + * @param opts.codeToReason - maps stream error codes to reasons + * @param opts.logger + * + * @throws {errors.ErrorQUICConnectionConfigInvalid} + */ public constructor({ type, scid, dcid, + serverName = null, remoteInfo, config, socket, reasonToCode = () => 0, codeToReason = (type, code) => new Error(`${type} ${code}`), - verifyCallback, logger, }: | { type: 'client'; scid: QUICConnectionId; - dcid?: undefined; + dcid?: void; + serverName?: string | null; remoteInfo: RemoteInfo; config: QUICConfig; socket: QUICSocket; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; logger?: Logger; } | { type: 'server'; scid: QUICConnectionId; dcid: QUICConnectionId; + serverName?: void; remoteInfo: RemoteInfo; config: QUICConfig; socket: QUICSocket; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; logger?: Logger; }) { - super(); this.logger = logger ?? new Logger(`${this.constructor.name} ${scid}`); - // Checking constraints if ( config.keepAliveIntervalTime != null && config.maxIdleTimeout !== 0 && config.keepAliveIntervalTime >= config.maxIdleTimeout ) { - throw new errors.ErrorQUICConnectionInvalidConfig( - 'keepAliveIntervalTime must be shorter than maxIdleTimeout', + throw new errors.ErrorQUICConnectionConfigInvalid( + '`keepAliveIntervalTime` must be less than `maxIdleTimeout`', ); } - const quicheConfig = buildQuicheConfig(config); let conn: Connection; if (type === 'client') { - // This message will be connected to the `this.start` this.logger.info(`Connect ${this.constructor.name}`); conn = quiche.Connection.connect( - null, + serverName, scid, { host: socket.host, @@ -383,7 +360,6 @@ class QUICConnection extends EventTarget { quicheConfig, ); } else if (type === 'server') { - // This message will be connected to `this.start` this.logger.info(`Accept ${this.constructor.name}`); conn = quiche.Connection.accept( scid, @@ -408,279 +384,317 @@ class QUICConnection extends EventTarget { this.connectionId = scid; this.socket = socket; this.config = config; + if (this.config.cert != null) { + const certPEMs = utils.collectPEMs(this.config.cert); + this.certDERs = certPEMs.map(utils.pemToDER); + } + if (this.config.ca != null) { + const caPEMs = utils.collectPEMs(this.config.ca); + this.caDERs = caPEMs.map(utils.pemToDER); + } this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; - this.verifyCallback = verifyCallback; this._remoteHost = remoteInfo.host; this._remotePort = remoteInfo.port; - const { - p: establishedP, - resolveP: resolveEstablishedP, - rejectP: rejectEstablishedP, - } = utils.promise(); - this.establishedP = establishedP; - this.resolveEstablishedP = resolveEstablishedP; - this.rejectEstablishedP = rejectEstablishedP; - const { p: secureEstablishedP, resolveP: resolveSecureEstablishedP, rejectP: rejectSecureEstablishedP, } = utils.promise(); this.secureEstablishedP = secureEstablishedP; - this.resolveSecureEstablishedP = resolveSecureEstablishedP; + this.resolveSecureEstablishedP = () => { + // This is an idempotent mutation + this.secureEstablished = true; + resolveSecureEstablishedP(); + }; this.rejectSecureEstablishedP = rejectSecureEstablishedP; - const { - p: closedP, - resolveP: resolveClosedP, - rejectP: rejectClosedP, - } = utils.promise(); + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); this.closedP = closedP; this.resolveClosedP = resolveClosedP; - this.rejectClosedP = rejectClosedP; } - public get remoteHost(): string { - return utils.fromHost(this._remoteHost); + public get remoteHost(): Host { + return this._remoteHost; } - public get remotePort(): number { - return utils.fromPort(this._remotePort); + public get remotePort(): Port { + return this._remotePort; } - public get localHost(): string { + public get localHost(): Host { return this.socket.host; } - public get localPort(): number { + public get localPort(): Port { return this.socket.port; } + public get closed() { + return this.conn.isClosed(); + } + /** - * This will set up the connection initiate sending - * @param data - the initial packet that triggered the creation of the connection. + * Starts the QUIC connection. + * + * This will complete the handshake and verify the peer's certificate. + * The connection will be ready to be used after this resolves. The peer's + * verification of the local certificate occurs concurrently. + * + * @param opts + * @param opts.data - server connections receive initial data + * @param opts.remoteInfo - server connections receive remote host and port + * @param ctx - timed context overrides `config.minIdleTimeout` which only + * works if `config.maxIdleTimeout` is greater than + * `config.minIdleTimeout` + * + * @throws {errors.ErrorQUICConnectionStartTimeout} - if timed out due to `ctx.timer` or `config.minIdleTimeout` + * @throws {errors.ErrorQUICConnectionStartData} - if no initial data for server connection + * @throws {errors.ErrorQUICConnection} - all other connection failure errors */ - public async start(data?: Uint8Array): Promise { + public start( + opts?: { + data?: Uint8Array; + remoteInfo?: RemoteInfo; + }, + ctx?: Partial, + ): PromiseCancellable; + @timedCancellable( + true, + minIdleTimeout, + errors.ErrorQUICConnectionStartTimeout, + ) + public async start( + { + data, + remoteInfo, + }: { + data?: Uint8Array; + remoteInfo?: RemoteInfo; + } = {}, + @context ctx: ContextTimed, + ): Promise { this.logger.info(`Start ${this.constructor.name}`); - // Set the connection up - this.socket.connectionMap.set(this.connectionId, this); - await withF( - [contextsUtils.monitor(this.lockbox, RWLockWriter)], - async ([mon]) => { - if (data != null) { - await this.recv( - data, - { host: this._remoteHost, port: this._remotePort }, - mon, - ); - } - await this.send(mon); - }, + // If the connection has already been closed, we cannot start it again + if (this.conn.isClosed()) { + throw new errors.ErrorQUICConnectionClosed(); + } + ctx.signal.throwIfAborted(); + const { p: abortP, rejectP: rejectAbortP } = utils.promise(); + const abortHandler = () => { + rejectAbortP(ctx.signal.reason); + }; + ctx.signal.addEventListener('abort', abortHandler); + this.addEventListener( + events.EventQUICConnectionError.name, + this.handleEventQUICConnectionError, ); + this.addEventListener( + events.EventQUICConnectionClose.name, + this.handleEventQUICConnectionClose, + { once: true }, + ); + if (this.type === 'client') { + await this.send(); + } else if (this.type === 'server') { + if (data == null || remoteInfo == null) { + throw new errors.ErrorQUICConnectionStartData( + 'Starting a server connection requires initial data and remote information', + ); + } + await this.recv(data, remoteInfo); + } + try { + // This will block until the connection is established + await Promise.race([this.secureEstablishedP, abortP]); + } catch (e) { + if (ctx.signal.aborted) { + // No `QUICStream` objects could have been created, however quiche stream + // state should be cleaned up, and this can be done synchronously + for (const streamId of this.conn.readable() as Iterable) { + this.conn.streamShutdown(streamId, quiche.Shutdown.Read, 0); + } + for (const streamId of this.conn.writable() as Iterable) { + this.conn.streamShutdown(streamId, quiche.Shutdown.Write, 0); + } + // According to RFC9000, closing while in the middle of a handshake + // should use a transport error code `APPLICATION_ERROR`. + // For this library we extend this "handshake" phase to include the + // the TLS handshake too. + // This is also the behaviour of quiche when the connection is not + // in a "safe" state to send application errors (where `app` parameter is true). + // https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.3-3 + this.conn.close( + false, + ConnectionErrorCode.ApplicationError, + Buffer.from(''), + ); + const localError = this.conn.localError()!; + const e_ = new errors.ErrorQUICConnectionLocal( + 'Failed to start QUIC connection due to start timeout', + { + data: localError, + cause: e, + }, + ); + this.dispatchEvent( + new events.EventQUICConnectionError({ + detail: e_, + }), + ); + } + // Wait for the connection to be fully closed by the `this.connTimeoutTimer` + await this.closedP; + // Throws original exception + throw e; + } finally { + ctx.signal.removeEventListener('abort', abortHandler); + } + if (this.config.keepAliveIntervalTime != null) { + this.startKeepAliveIntervalTimer(this.config.keepAliveIntervalTime); + } this.logger.info(`Started ${this.constructor.name}`); } /** - * The `applicationError` if the connection close is due to the transport - * layer or due to the application layer. - * If `applicationError` is true, you can use any number as the `errorCode`. - * The other peer must should understand the `errorCode`. - * If `applicationError` is false, you must use `errorCode` from - * `ConnectionErrorCode`. - * The default `applicationError` is true because a normal graceful close - * is an application error. - * The default `errorCode` of 0 means general error. - * This is the same as basically waiting for `closedP`. + * Stops the QUIC connection. * - * Providing error details is only used if the connection still needs to be - * closed. If stop was triggered internally then the error details are obtained - * by the connection. + * @param opts + * @param opts.isApp - whether the destroy is initiated by the application + * @param opts.errorCode - the error code to send to the peer, use + * `ConnectionErrorCode` if `isApp` is false, + * otherwise any unsigned integer is fine. + * @param opts.reason - the reason to send to the peer + * @param opts.force - force controls whether to cancel streams or wait for + * streams to close gracefully + * + * The provided error parameters is only used if the quiche connection is not + * draining or closed. */ - public async stop( - { - applicationError = true, - errorCode = 0, - errorMessage = '', - force = false, - }: - | { - applicationError?: false; - errorCode?: ConnectionErrorCode; - errorMessage?: string; - force?: boolean; - } - | { - applicationError: true; - errorCode?: number; - errorMessage?: string; - force?: boolean; - } = {}, - mon?: Monitor, - ) { - this.logger.info(`Stop ${this.constructor.name}`); - // Cleaning up existing streams - const streamsDestroyP: Array> = []; - this.logger.debug('triggering stream destruction'); - for (const stream of this.streamMap.values()) { - // If we're draining then streams will never end on their own. - // We must force them to end. - if (this.conn.isDraining() || this.conn.isClosed() || force) { - await stream.destroy(); + public async stop({ + isApp = true, + errorCode = 0, + reason = new Uint8Array(), + force = true, + }: + | { + isApp: false; + errorCode?: ConnectionErrorCode; + reason?: Uint8Array; + force?: boolean; } - streamsDestroyP.push(stream.destroyedP); - } - this.logger.debug('waiting for streams to destroy'); - await Promise.all(streamsDestroyP); - this.logger.debug('streams destroyed'); + | { + isApp?: true; + errorCode?: number; + reason?: Uint8Array; + force?: boolean; + } = {}) { + this.logger.info(`Stop ${this.constructor.name}`); this.stopKeepAliveIntervalTimer(); - - // Trigger closing connection in the background and await close later. - void this.withMonitor(mon, this.lockbox, RWLockWriter, async (mon) => { - await mon.withF(this.lockCode, async (mon) => { - // If this is already closed, then `Done` will be thrown - // Otherwise it can send `CONNECTION_CLOSE` frame - // This can be 0x1c close at the QUIC layer or no errors - // Or it can be 0x1d for application close with an error - // Upon receiving a `CONNECTION_CLOSE`, you can send back - // 1 packet containing a `CONNECTION_CLOSE` frame too - // (with `NO_ERROR` code if appropriate) - // It must enter into a draining state, and no other packets can be sent - try { - this.conn.close( - applicationError, - errorCode, - Buffer.from(errorMessage), - ); - // If we get a `Done` exception we don't bother calling send - // The send only gets sent if the `Done` is not the case - await this.send(mon); - } catch (e) { - // Ignore 'Done' if already closed - if (e.message !== 'Done') { - // No other exceptions are expected - never(); - } - } + // Closing the connection first to avoid accepting new streams + if (!this.conn.isDraining() && !this.conn.isClosed()) { + // If `this.conn.close` is already called, the connection will be draining, + // in that case we just skip doing this local close. + // If `this.conn.isTimedOut` is true, then the connection will be closed, + // in that case we skip doing this local close. + this.conn.close(isApp, errorCode, reason); + const localError = this.conn.localError()!; + const message = `Locally closed with ${ + localError.isApp ? 'application' : 'transport' + } code ${localError.errorCode}`; + const e = new errors.ErrorQUICConnectionLocal(message, { + data: localError, }); - }); - - if (this.conn.isClosed()) { - this.resolveClosedP(); + this.dispatchEvent(new events.EventQUICConnectionError({ detail: e })); } - this.setConnTimeOutTimer(); - // Now we await for the closedP - this.logger.debug('awaiting closedP'); - await this.closedP; - this.logger.debug('closedP'); - this.connTimeOutTimer?.cancel(timerCleanupReasonSymbol); - - // Removing the connection from the socket's connection map - this.socket.connectionMap.delete(this.connectionId); - - // Checking for errors and emitting them as events - // Emit error if connection timed out - if (this.conn.isTimedOut()) { - const error = this.secured - ? new errors.ErrorQUICConnectionIdleTimeOut() - : new errors.ErrorQUICConnectionStartTimeOut(); - - this.rejectEstablishedP(error); - this.rejectSecureEstablishedP(error); - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: error, + // Destroy all streams + const streamsDestroyP: Array> = []; + for (const quicStream of this.streamMap.values()) { + // The reason is only used if `force` is `true` + // If `force` is not true, this will gracefully wait for + // both readable and writable to gracefully close + streamsDestroyP.push( + quicStream.destroy({ + reason: this.errorLast, + force: force || this.conn.isDraining() || this.conn.isClosed(), }), ); } + await Promise.all(streamsDestroyP); + // Waiting for `closedP` to resolve + // Only the `this.connTimeoutTimer` will resolve this promise + await this.closedP; + this.removeEventListener( + events.EventQUICConnectionError.name, + this.handleEventQUICConnectionError, + ); + this.removeEventListener( + events.EventQUICConnectionClose.name, + this.handleEventQUICConnectionClose, + ); + this.logger.info(`Stopped ${this.constructor.name}`); + } - // Emit error if peer error - const peerError = this.conn.peerError(); - if (peerError != null) { - const message = `Connection errored out with peerError ${Buffer.from( - peerError.reason, - ).toString()}(${peerError.errorCode})`; - this.logger.info(message); - const error = new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...peerError, - }, - }); - this.rejectEstablishedP(error); - this.rejectSecureEstablishedP(error); - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: error, - }), - ); - } + /** + * Get connection error. + * This could be `undefined` if connection timed out. + */ + public getConnectionError(): ConnectionError | undefined { + return this.conn.localError() ?? this.conn.peerError() ?? undefined; + } - // Emit error if local error - const localError = this.conn.localError(); - if (localError != null) { - const message = `connection failed with localError ${Buffer.from( - localError.reason, - ).toString()}(${localError.errorCode})`; - this.logger.info(message); - const error = new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...localError, - }, - }); - this.rejectEstablishedP(error); - this.rejectSecureEstablishedP(error); - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: error, - }), - ); - } + /** + * Array of independent CA certificates in DER format. + */ + public getLocalCACertsChain(): Array { + return this.caDERs; + } - this.dispatchEvent(new events.QUICConnectionStopEvent()); - this.logger.info(`Stopped ${this.constructor.name}`); + /** + * Chain of local certificates from leaf to root in DER format. + */ + public getLocalCertsChain(): Array { + return this.certDERs; } /** - * Gets an array of certificates in PEM format start on the leaf. + * Chain of remote certificates from leaf to root in DER format. */ - @ready(new errors.ErrorQUICConnectionNotRunning()) - public getRemoteCertsChain(): Array { - const certsDER = this.conn.peerCertChain(); - if (certsDER == null) return []; - return certsDER.map(utils.certificateDERToPEM); + public getRemoteCertsChain(): Array { + return this.conn.peerCertChain() ?? []; + } + + public meta(): ConnectionMetadata { + return { + localHost: this.localHost, + localPort: this.localPort, + remoteHost: this.remoteHost, + remotePort: this.remotePort, + localCertsChain: this.certDERs, + localCACertsChain: this.caDERs, + remoteCertsChain: this.getRemoteCertsChain(), + }; } /** - * Called when the socket receives data from the remote side intended for this connection. - * UDP -> Connection -> Stream - * This pushes data to the streams. + * Receives data from the socket for this connection. + * + * The data flows from the socket this connection to streams. + * This takes data from the quiche connection and pushes to the + * `QUICStream` collection. + * + * This function is callable during `this.start` and `this.stop`. * When the connection is draining, we can still receive data. - * However, no streams are allowed to read or write. + * However no streams are allowed to read or write data. * - * This method must not throw any exceptions. - * Any errors must be emitted as events. * @internal */ - public async recv( - data: Uint8Array, - remoteInfo: RemoteInfo, - mon?: Monitor, - ): Promise { - await this.withMonitor(mon, this.lockbox, RWLockWriter, async (mon) => { - if (!mon.isLocked(this.lockCode)) { - return mon.withF(this.lockCode, async (mon) => { - return this.recv(data, remoteInfo, mon); - }); - } - }); - - try { - // The remote information may be changed on each receive - // However to do so would mean connection migration, - // which is not yet supported - this._remoteHost = remoteInfo.host; - this._remotePort = remoteInfo.port; + @ready(new errors.ErrorQUICConnectionNotRunning(), false, [ + 'starting', + 'stopping', + ]) + public async recv(data: Uint8Array, remoteInfo: RemoteInfo): Promise { + // Enforce mutual exclusion for an atomic pair of `this.recv` and `this.send`. + await this.recvLock.withF(async () => { const recvInfo = { to: { host: this.localHost, @@ -692,352 +706,434 @@ class QUICConnection extends EventTarget { }, }; try { - this.logger.debug(`recv ${data.byteLength} bytes`); - // This can process concatenated QUIC packets - // This may mutate `data` + // This can process multiple QUIC packets. + // Remember that 1 QUIC packet can have multiple QUIC frames. + // Expect the `data` is mutated here due to in-place decryption, + // so do not re-use the `data` afterwards. this.conn.recv(data, recvInfo); } catch (e) { - // Should only be a `TLSFail` if we fail here. - // The error details will be available as a local error. - if (e.message !== 'TlsFail') { - // No other exceptions are expected - never(); - } - } - - // Checking if the packet was a short frame. - // Short indicates that the peer has completed TLS verification - if (!this.shortReceived) { - const header = quiche.Header.fromSlice(data, quiche.MAX_CONN_ID_LEN); - // If short frame - if (header.ty === 5) { - this.shortReceived = true; - } - } - // Checks if `secureEstablishedP` should be resolved. The condition for - // this is if a short frame has been received and 1 extra frame has been - // received. This allows for the remote to close the connection. - if (!this.secured && this.shortReceived && !this.conn.isDraining()) { - if (this.count >= 1) { - this.secured = true; - this.resolveSecureEstablishedP(); + // If `config.verifyPeer` is true and `config.verifyCallback` is undefined, + // then during the TLS handshake, a `TlsFail` exception will only be thrown + // if the peer did not supply a certificate or that its certificate failed + // the default certificate verification procedure. + // If `config.verifyPeer` is true and `config.verifyCallback` is defined, + // then during the TLS handshake, a `TlsFail` exception will only be thrown + // if the peer did not supply a peer certificate. + const localError = this.conn.localError(); + if (localError == null) { + // If there was no local error, then this is an internal error. + const e_ = new errors.ErrorQUICConnectionInternal( + 'Failed connection recv due with unknown error', + { cause: e }, + ); + this.dispatchEvent( + new events.EventQUICConnectionError({ detail: e_ }), + ); + throw e_; + } else { + // Quiche connection recv will automatically close with local + // connection error and start draining. + // This is a legitimate state transition. + let e_: errors.ErrorQUICConnectionLocal; + if (e.message === 'TlsFail') { + e_ = new errors.ErrorQUICConnectionLocalTLS( + 'Failed connection due to native TLS verification', + { + cause: e, + data: localError, + }, + ); + } else { + e_ = new errors.ErrorQUICConnectionLocal( + 'Failed connection due to local error', + { + cause: e, + data: localError, + }, + ); + } + this.dispatchEvent( + new events.EventQUICConnectionError({ detail: e_ }), + ); + return; } - this.count += 1; - } - - // We don't actually "fail" - // the closedP until we proceed - // But note that if there's an error - - if (this.conn.isEstablished()) { - this.resolveEstablishedP(); - } - - if (this.conn.isClosed()) { - this.resolveClosedP(); - return; } - - if (this.conn.isInEarlyData() || this.conn.isEstablished()) { - await this.processStreams(); - } - } finally { - this.logger.debug('RECV FINALLY'); + // The remote information may be changed on each received packet. + // If it changes, this would mean the connection has migrated. + this._remoteHost = remoteInfo.host; + this._remotePort = remoteInfo.port; + // If `config.verifyCallback` is not defined, simply being established is + // sufficient to mean we are securely established, however if it is defined + // then secure establishment occurs only after custom TLS verification has + // passed. if ( - this[status] !== 'destroying' && - (this.conn.isClosed() || this.conn.isDraining()) + !this.secureEstablished && + this.conn.isEstablished() && + this.config.verifyCallback == null ) { - this.logger.debug('calling stop due to closed or draining'); - // Destroy in the background, we still need to process packets. - // Draining means no more packets are sent, so streams must be force closed. - void this.stop({ force: true }, mon).catch(() => {}); + this.resolveSecureEstablishedP(); } - } + // If we are securely established we can process streams. + if (this.secureEstablished) { + this.processStreams(); + } + // After every recv, there must be a send. + await this.send(); + }); } /** - * Called when the socket has to send back data on this connection. - * This is better understood as "flushing" the connection send buffer. - * This is because the data to send actually comes from the quiche library - * and any data that is currently buffered on the streams. - * It will send everything into the UDP socket. + * Sends data to the socket from this connection. * - * UDP <- Connection <- Stream + * This takes the data from the quiche connection that is on the send buffer. + * The data flows from the streams to this connection to the socket. * - * Call this if `recv` is called. - * Call this if timer expires. - * Call this if stream is written. - * Call this if stream is read. + * - Call this if connecting to a server. + * - Call this if `recv` is called. + * - Call this if `connTimeoutTimer` ticks. + * - Call this if stream is written. + * - Call this if stream is read. * - * We can push the connection into the stream. - * The streams have access to the connection object. + * This function is callable during `this.start` and `this.stop`. + * When the connection is draining, we can still receive data. + * However no streams are allowed to read or write data. * - * This method must not throw any exceptions. - * Any errors must be emitted as events. * @internal */ - public async send(mon?: Monitor): Promise { - await this.withMonitor(mon, this.lockbox, RWLockWriter, async (mon) => { - if (!mon.isLocked(this.lockCode)) { - return mon.withF(this.lockCode, async (mon) => { - return this.send(mon); - }); - } - }); - - const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE); + @ready(new errors.ErrorQUICConnectionNotRunning(), false, [ + 'starting', + 'stopping', + ]) + public async send(): Promise { let sendLength: number; let sendInfo: SendInfo; - try { - // Send until `Done` - while (true) { - try { - [sendLength, sendInfo] = this.conn.send(sendBuffer); - } catch (e) { - if (e.message === 'Done') { - break; - } - throw e; + // Send until `Done` + while (true) { + // Fastest way of allocating a buffer, which will be dispatched + const sendBuffer = Buffer.allocUnsafe(this.config.maxSendUdpPayloadSize); + try { + const result = this.conn.send(sendBuffer); + if (result === null) { + // Break the loop + break; } - await this.socket.send( - sendBuffer, - 0, - sendLength, - sendInfo.to.port, - sendInfo.to.host, + [sendLength, sendInfo] = result; + } catch (e) { + // Internal error if send failed + const e_ = new errors.ErrorQUICConnectionInternal( + 'Failed connection send with unknown internal error', + { cause: e }, ); - this.logger.debug(`sent ${sendLength} bytes`); - - // Handling custom TLS verification, this must be done after the following conditions. - // 1. Connection established. - // 2. Certs available. - // 3. Sent after connection has established. + this.dispatchEvent(new events.EventQUICConnectionError({ detail: e_ })); + throw e_; + } + this.dispatchEvent( + new events.EventQUICConnectionSend({ + detail: { + msg: sendBuffer.subarray(0, sendLength), + port: sendInfo.to.port, + address: sendInfo.to.host, + }, + }), + ); + } + // Resets the `this.connTimeoutTimer` because quiche timeout becomes + // non-null after the first send call is made, and subsequently, each + // send call may end up resetting the quiche timeout value. + this.setConnTimeoutTimer(); + if ( + !this.secureEstablished && + !this.conn.isDraining() && + !this.conn.isClosed() && + this.conn.isEstablished() && + this.config.verifyPeer && + this.config.verifyCallback != null + ) { + const peerCertsChain = this.conn.peerCertChain()!; + // Custom TLS verification + const cryptoError = await this.config.verifyCallback( + peerCertsChain, + this.caDERs, + ); + if (cryptoError != null) { + // This simulates the crypto error that occurs natively + this.conn.close(false, cryptoError, Buffer.from('')); + const localError = this.conn.localError()!; + const e_ = new errors.ErrorQUICConnectionLocalTLS( + 'Failed connection due to custom TLS verification', + { + data: localError, + }, + ); + this.dispatchEvent( + new events.EventQUICConnectionError({ + detail: e_, + }), + ); + return; + } + // If this succeeds, then we have securely established, and we can + // process all the streams, that would have originally occurred in + // `this.recv`. This will only be run on the first time, we perform + // the custom TLS verification + this.resolveSecureEstablishedP(); + this.processStreams(); + } + if (this[status] !== 'stopping') { + const peerError = this.conn.peerError(); + if (peerError != null) { + const message = `Peer closed with ${ + peerError.isApp ? 'application' : 'transport' + } code ${peerError.errorCode}`; if ( - !this.customVerified && - this.conn.isEstablished() && - this.conn.peerCertChain() != null + peerError.errorCode >= quiche.CRYPTO_ERROR_START && + peerError.errorCode <= quiche.CRYPTO_ERROR_STOP ) { - this.customVerified = true; - const peerCerts = this.conn.peerCertChain(); - if (peerCerts == null) never(); - const peerCertsPem = peerCerts.map((c) => - utils.certificateDERToPEM(c), + this.dispatchEvent( + new events.EventQUICConnectionError({ + detail: new errors.ErrorQUICConnectionPeerTLS(message, { + data: peerError, + }), + }), + ); + } else { + this.dispatchEvent( + new events.EventQUICConnectionError({ + detail: new errors.ErrorQUICConnectionPeer(message, { + data: peerError, + }), + }), ); - try { - // Running verify callback if available - if (this.verifyCallback != null) { - await this.verifyCallback(peerCertsPem); - } - this.logger.debug('TLS verification succeeded'); - // Generate ack frame to satisfy the short + 1 condition of secure establishment - this.conn.sendAckEliciting(); - } catch (e) { - // Force the connection to end. - // Error 304 indicates cert chain failed verification. - // Error 372 indicates cert chain was missing. - this.logger.debug( - `TLS fail due to [${e.message}], closing connection`, - ); - this.conn.close( - false, - 304, - Buffer.from(`Custom TLSFail: ${e.message}`), - ); - } } + return; } - } catch (e) { - // An error here means a hard failure in sending, we must force clean up - // since any further communication is expected to fail. - this.logger.debug(`Calling stop due to sending error [${e.message}]`); - const code = await this.reasonToCode('send', e); - await this.stop( - { - applicationError: false, - errorCode: code, - errorMessage: e.message, - force: true, - }, - mon, - ); - // We need to finish without any exceptions - return; - } - if (this.conn.isClosed()) { - // Handle stream clean up if closed - this.resolveClosedP(); - await this.stop( - this.conn.localError() ?? this.conn.peerError() ?? {}, - mon, - ); } - this.setConnTimeOutTimer(); } - /** - * Keeps stream processing logic all in one place. - */ - protected async processStreams() { + protected processStreams() { for (const streamId of this.conn.readable() as Iterable) { let quicStream = this.streamMap.get(streamId); if (quicStream == null) { - // The creation will set itself to the stream map - quicStream = await QUICStream.createQUICStream({ + quicStream = QUICStream.createQUICStream({ + initiated: 'peer', streamId, + config: this.config, connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), }); + this.streamMap.set(quicStream.streamId, quicStream); + quicStream.addEventListener( + events.EventQUICStreamSend.name, + this.handleEventQUICStreamSend, + ); quicStream.addEventListener( - 'streamDestroy', - this.handleQUICStreamDestroyEvent, + events.EventQUICStreamDestroyed.name, + this.handleEventQUICStreamDestroyed, { once: true }, ); + quicStream.addEventListener(EventAll.name, this.handleEventQUICStream); this.dispatchEvent( - new events.QUICConnectionStreamEvent({ detail: quicStream }), + new events.EventQUICConnectionStream({ detail: quicStream }), ); - // No need to read after creation, doing so will throw during early cancellation - } else { - quicStream.read(); } + quicStream.read(); } for (const streamId of this.conn.writable() as Iterable) { - const quicStream = this.streamMap.get(streamId); + let quicStream = this.streamMap.get(streamId); if (quicStream == null) { - // This is a dead case, there are only two ways streams are created. - // The QUICStream will always exist before processing it's writable. - // 1. First time it is seen in the readable iterator - // 2. created using `streamNew()` - - // There is one condition where this can happen. That is when both sides of the stream cancel concurrently. - // Local state is cleaned up while the remote side still sends a closing frame. - try { - // Check if the stream can write 0 bytes, should throw if the stream has ended. - // We need to check if it's writable to trigger any state change for the stream. - this.conn.streamWritable(streamId, 0); - never( - 'The stream should never be writable if a QUICStream does not exist for it', - ); - } catch (e) { - // Stream should be stopped here, any other error is a never - if (e.message.match(/StreamStopped\((.+)\)/) == null) { - // We only expect a StreamStopped error here - throw e; - } - // If stopped we just ignore it, `streamWritable` should've cleaned up the native state - this.logger.debug( - `StreamId ${streamId} was writable without an existing stream and error ${e.message}`, - ); - } - } else { - quicStream.write(); + quicStream = QUICStream.createQUICStream({ + initiated: 'peer', + streamId, + config: this.config, + connection: this, + codeToReason: this.codeToReason, + reasonToCode: this.reasonToCode, + logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), + }); + this.streamMap.set(quicStream.streamId, quicStream); + quicStream.addEventListener( + events.EventQUICStreamSend.name, + this.handleEventQUICStreamSend, + ); + quicStream.addEventListener( + events.EventQUICStreamDestroyed.name, + this.handleEventQUICStreamDestroyed, + { once: true }, + ); + quicStream.addEventListener(EventAll.name, this.handleEventQUICStream); + this.dispatchEvent( + new events.EventQUICConnectionStream({ detail: quicStream }), + ); } + quicStream.write(); } } - protected setConnTimeOutTimer(): void { - const logger = this.logger.getChild('timer'); - const connTimeOutHandler = async () => { + /** + * Sets up the connection timeout timer. + * + * This only gets called on the first `QUICConnection.send`. + * It's the responsiblity of this timer to resolve the `closedP`. + */ + protected setConnTimeoutTimer(): void { + const connTimeoutHandler = async (signal: AbortSignal) => { + // If aborted, just immediately resolve + if (signal.aborted) return; // This can only be called when the timeout has occurred. // This transitions the connection state. // `conn.timeout()` is time aware, so calling `conn.onTimeout` will only // trigger state transitions after the time has passed. - logger.debug('CALLING ON TIMEOUT'); this.conn.onTimeout(); - - // Connection may have closed after timing out + // If it is closed, we can resolve, and we are done for this connection. if (this.conn.isClosed()) { - // If it was still starting waiting for the secure event, we need to - // reject the `secureEstablishedP` promise. - if (this[status] === 'starting') { - this.rejectSecureEstablishedP( - new errors.ErrorQUICConnectionInternal('Connection has closed!'), - ); - } - - logger.debug('resolving closedP'); - // We resolve closing here, stop checks if the connection has timed out - // and handles it. this.resolveClosedP(); - // If we are still running and not stopping then we need to stop - if (this[running] && this[status] !== 'stopping') { - // Background stopping, we don't want to block the timer resolving - void this.stop({ force: true }); + // Check if the connection timed out due to the `maxIdleTimeout` + if (this.conn.isTimedOut()) { + this.dispatchEvent( + new events.EventQUICConnectionError({ + detail: new errors.ErrorQUICConnectionIdleTimeout(), + }), + ); } - logger.debug('CLEANING UP TIMER'); return; } - - // There may be data to send after timing out - void this.send(); - + await this.send(); // Note that a `0` timeout is still a valid timeout const timeout = this.conn.timeout(); - // If this is `null`, then quiche is requesting the timer to be cleaned up + // If the max idle timeout is 0, then the timeout may be `null`, + // and it would only be set when the connection is ready to be closed. + // If it is `null`, there is no need to setup the next timer if (timeout == null) { - logger.debug('CLEANING UP TIMER'); return; } - // Allow an extra 1ms for the delay to fully complete, so we can avoid a repeated 0ms delay - logger.debug(`Recreating timer with ${timeout + 1} delay`); - this.connTimeOutTimer = new Timer({ + // Allow an extra 1ms to compensate for clock desync with quiche + this.connTimeoutTimer = new Timer({ delay: timeout + 1, - handler: connTimeOutHandler, + handler: connTimeoutHandler, + lazy: true, }); }; // Note that a `0` timeout is still a valid timeout const timeout = this.conn.timeout(); // If this is `null`, then quiche is requesting the timer to be cleaned up if (timeout == null) { - // Clean up timer if it is running - if ( - this.connTimeOutTimer != null && - this.connTimeOutTimer.status === null - ) { - logger.debug('CLEANING UP TIMER'); - this.connTimeOutTimer.cancel(timerCleanupReasonSymbol); + // Cancellation only matters if the timer status is `null` or settling + // If it is `null`, then the timer handler doesn't run + // If it is `settled`, then cancelling is a noop + // If it is `settling`, then cancelling only prevents it at the beginning of the handler + this.connTimeoutTimer?.cancel(); + // The `this.connTimeoutTimer` is a lazy timer, so it's status may still + // be `null` or `settling`. So we have to delete it here to ensure that + // the timer will be recreated. + delete this.connTimeoutTimer; + if (this.conn.isClosed()) { + this.resolveClosedP(); + if (this.conn.isTimedOut()) { + this.dispatchEvent( + new events.EventQUICConnectionError({ + detail: new errors.ErrorQUICConnectionIdleTimeout(), + }), + ); + } } return; } - // If there was an existing timer, we cancel it and set a new one + // If there's no timer, create it + // If the timer is settled, create it + // If the timer is null, reset it + // If the timer is settling, do nothing (it will recreate itself) + // Plus 1 to the `timeout` to compensate for clock desync with quiche if ( - this.connTimeOutTimer != null && - this.connTimeOutTimer.status === null + this.connTimeoutTimer == null || + this.connTimeoutTimer.status === 'settled' ) { - logger.debug(`resetting timer with ${timeout + 1} delay`); - this.connTimeOutTimer.reset(timeout + 1); - } else { - logger.debug(`timeout created with delay ${timeout}`); - this.connTimeOutTimer = new Timer({ + this.connTimeoutTimer = new Timer({ delay: timeout + 1, - handler: connTimeOutHandler, + handler: connTimeoutHandler, + lazy: true, }); + } else if (this.connTimeoutTimer.status == null) { + this.connTimeoutTimer.reset(timeout + 1); } } + /** + * Creates a new QUIC stream on the connection. + */ + @ready(new errors.ErrorQUICConnectionNotRunning()) + public newStream(type: 'bidi' | 'uni' = 'bidi'): QUICStream { + let streamId: StreamId; + if (this.type === 'client' && type === 'bidi') { + streamId = this.streamIdClientBidi; + } else if (this.type === 'server' && type === 'bidi') { + streamId = this.streamIdServerBidi; + } else if (this.type === 'client' && type === 'uni') { + streamId = this.streamIdClientUni; + } else if (this.type === 'server' && type === 'uni') { + streamId = this.streamIdServerUni; + } + const quicStream = QUICStream.createQUICStream({ + initiated: 'local', + streamId: streamId!, + connection: this, + config: this.config, + codeToReason: this.codeToReason, + reasonToCode: this.reasonToCode, + logger: this.logger.getChild(`${QUICStream.name} ${streamId!}`), + }); + this.streamMap.set(quicStream.streamId, quicStream); + quicStream.addEventListener( + events.EventQUICStreamSend.name, + this.handleEventQUICStreamSend, + ); + quicStream.addEventListener( + events.EventQUICStreamDestroyed.name, + this.handleEventQUICStreamDestroyed, + { once: true }, + ); + quicStream.addEventListener(EventAll.name, this.handleEventQUICStream); + if (this.type === 'client' && type === 'bidi') { + this.streamIdClientBidi = (this.streamIdClientBidi + 4) as StreamId; + } else if (this.type === 'server' && type === 'bidi') { + this.streamIdServerBidi = (this.streamIdServerBidi + 4) as StreamId; + } else if (this.type === 'client' && type === 'uni') { + this.streamIdClientUni = (this.streamIdClientUni + 4) as StreamId; + } else if (this.type === 'server' && type === 'uni') { + this.streamIdServerUni = (this.streamIdServerUni + 4) as StreamId; + } + return quicStream; + } + /** * Starts the keep alive interval timer. - * Make sure to set the interval to be less than then the `maxIdleTime` unless - * if the `maxIdleTime` is `0`. - * If the `maxIdleTime` is `0`, then this is not needed to keep the connection - * open. However, it can still be useful to maintain liveliness for NAT purposes. + * + * Interval time should be less than `maxIdleTimeout` unless it is `0`, which + * means `Infinity`. + * + * If the `maxIdleTimeout` is `0`, this can still be useful to maintain + * activity on the connection. */ protected startKeepAliveIntervalTimer(ms: number): void { - const keepAliveHandler = async () => { + const keepAliveHandler = async (signal: AbortSignal) => { + if (signal.aborted) return; // Intelligently schedule a PING frame. // If the connection has already sent ack-eliciting frames // then this is a noop. this.conn.sendAckEliciting(); await this.send(); + if (signal.aborted) return; this.keepAliveIntervalTimer = new Timer({ delay: ms, handler: keepAliveHandler, + lazy: true, }); }; this.keepAliveIntervalTimer = new Timer({ delay: ms, handler: keepAliveHandler, + lazy: true, }); } @@ -1045,65 +1141,7 @@ class QUICConnection extends EventTarget { * Stops the keep alive interval timer */ protected stopKeepAliveIntervalTimer(): void { - this.keepAliveIntervalTimer?.cancel(timerCleanupReasonSymbol); - } - - /** - * Creates a new stream on the connection. - * Only supports bidi streams atm. - * This is a serialised call, it must be blocking. - */ - @ready(new errors.ErrorQUICConnectionNotRunning()) - public async streamNew(streamType: 'bidi' = 'bidi'): Promise { - // Using a lock on stream ID to prevent racing updates - return await this.streamIdLock.withF(async () => { - let streamId: StreamId; - if (this.type === 'client' && streamType === 'bidi') { - streamId = this.streamIdClientBidi; - } else if (this.type === 'server' && streamType === 'bidi') { - streamId = this.streamIdServerBidi; - } - - const quicStream = await QUICStream.createQUICStream({ - streamId: streamId!, - connection: this, - codeToReason: this.codeToReason, - reasonToCode: this.reasonToCode, - logger: this.logger.getChild(`${QUICStream.name} ${streamId!}`), - }); - quicStream.addEventListener( - 'streamDestroy', - this.handleQUICStreamDestroyEvent, - { once: true }, - ); - // Ok the stream is opened and working - if (this.type === 'client' && streamType === 'bidi') { - this.streamIdClientBidi = (this.streamIdClientBidi + 4) as StreamId; - } else if (this.type === 'server' && streamType === 'bidi') { - this.streamIdServerBidi = (this.streamIdServerBidi + 4) as StreamId; - } - return quicStream; - }); - } - - /** - * Used as a clean way to create a new monitor if it doesn't exist, otherwise uses the existing one. - */ - protected async withMonitor( - mon: Monitor | undefined, - lockBox: LockBox, - lockConstructor: { new (): RWLockWriter }, - f: (mon: Monitor) => Promise, - locksPending?: Map, - ): Promise { - if (mon == null) { - return await withF( - [contextsUtils.monitor(lockBox, lockConstructor, locksPending)], - ([mon]) => f(mon), - ); - } else { - return f(mon); - } + this.keepAliveIntervalTimer?.cancel(); } } diff --git a/src/QUICServer.ts b/src/QUICServer.ts index 8d533f5b..b295d378 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -1,107 +1,258 @@ import type { Host, + Port, RemoteInfo, + QUICServerCrypto, + ResolveHostname, + QUICConfig, + QUICServerConfigInput, StreamCodeToReason, StreamReasonToCode, - QUICConfig, - ServerCrypto, - VerifyCallback, } from './types'; import type { Header } from './native/types'; -import type QUICConnectionMap from './QUICConnectionMap'; import Logger from '@matrixai/logger'; -import { running } from '@matrixai/async-init'; -import { ready, StartStop } from '@matrixai/async-init/dist/StartStop'; -import * as events from './events'; -import { serverDefault } from './config'; -import QUICConnectionId from './QUICConnectionId'; +import { AbstractEvent, EventAll } from '@matrixai/events'; +import { + StartStop, + ready, + running, + status, +} from '@matrixai/async-init/dist/StartStop'; +import QUICSocket from './QUICSocket'; import QUICConnection from './QUICConnection'; -import { quiche } from './native'; +import QUICConnectionId from './QUICConnectionId'; +import { quiche, ConnectionErrorCode } from './native'; +import { serverDefault } from './config'; import * as utils from './utils'; +import * as events from './events'; import * as errors from './errors'; -import QUICSocket from './QUICSocket'; -import { never } from './utils'; -/** - * You must provide an error handler `addEventListener('error')`. - * Otherwise, errors will just be ignored. - * - * Events: - * - serverStop - * - serverError - (could be a QUICSocketErrorEvent OR QUICServerErrorEvent) - * - serverConnection - * - connectionStream - when new stream is created from a connection - * - connectionError - connection error event - * - connectionDestroy - when connection is destroyed - * - streamDestroy - when stream is destroyed - * - socketError - this also results in a server error - * - socketStop - */ interface QUICServer extends StartStop {} -@StartStop() -class QUICServer extends EventTarget { +@StartStop({ + eventStart: events.EventQUICServerStart, + eventStarted: events.EventQUICServerStarted, + eventStop: events.EventQUICServerStop, + eventStopped: events.EventQUICServerStopped, +}) +class QUICServer { + /** + * Determines if socket is shared. + */ public readonly isSocketShared: boolean; + /** + * Custom reason to code converter for new connections. + */ + public reasonToCode?: StreamReasonToCode; + + /** + * Custom code to reason converted for new connections. + */ + public codeToReason?: StreamCodeToReason; + + /** + * The minimum idle timeout to be used for new connections. + */ + public minIdleTimeout?: number; + protected logger: Logger; - protected crypto: { - key: ArrayBuffer; - ops: ServerCrypto; - }; - protected config: QUICConfig; protected socket: QUICSocket; - protected reasonToCode: StreamReasonToCode | undefined; - protected codeToReason: StreamCodeToReason | undefined; - protected verifyCallback: VerifyCallback | undefined; - protected connectionMap: QUICConnectionMap; - protected minIdleTimeout: number | undefined; - // Used to track address string for logging ONLY - protected address: string; + protected crypto: QUICServerCrypto; + protected config: QUICConfig; + protected _closed: boolean = false; + protected _closedP: Promise; + protected resolveClosedP: () => void; - protected handleQUICSocketEvents = (e: events.QUICSocketEvent) => { - if (e instanceof events.QUICSocketErrorEvent) { - this.dispatchEvent( - new events.QUICServerErrorEvent({ - detail: e.detail, - }), - ); - // Trigger clean up - this.logger.debug('calling stop due to socket error'); - void this.stop({ force: true }); - } else if (e instanceof events.QUICSocketStopEvent) { - this.dispatchEvent(new events.QUICSocketStopEvent()); - // Trigger clean up - this.logger.debug('calling stop due to socket stop'); - void this.stop({ force: true }); - } else if (e instanceof events.QUICSocketStartEvent) { - this.dispatchEvent(new events.QUICSocketStartEvent()); - } else { - // Should never happen, all cases should be covered - never(); + /** + * Handles `EventQUICServerError`. + * + * Internal errors will be thrown upwards to become an uncaught exception. + * + * @throws {errors.ErrorQUICServerInternal} + */ + protected handleEventQUICServerError = (evt: events.EventQUICServerError) => { + const error = evt.detail; + this.logger.error(utils.formatError(error)); + if (error instanceof errors.ErrorQUICServerInternal) { + throw error; } + this.dispatchEvent( + new events.EventQUICServerClose({ + detail: error, + }), + ); }; - protected handleQUICConnectionEvents = ( - event: events.QUICConnectionEvent, + /** + * Handles `EventQUICServerClose`. + * Registered once. + * + * This event propagates errors minus the internal errors. + * + * If this event is dispatched first before `QUICServer.stop`, it represents + * an evented close. This could originate from the `QUICSocket`. If it was + * from the `QUICSocket`, then here it will stop all connections with an + * transport code `InternalError`. + */ + protected handleEventQUICServerClose = async ( + evt: events.EventQUICServerClose, ) => { - if (event instanceof events.QUICConnectionErrorEvent) { - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: event.detail, - }), + const error = evt.detail; + if (!(error instanceof errors.ErrorQUICServerSocketNotRunning)) { + // Only stop the socket if it was encapsulated + if (!this.isSocketShared) { + // Remove the stopped listener, as we intend to stop the socket + this.socket.removeEventListener( + events.EventQUICSocketStopped.name, + this.handleEventQUICSocketStopped, + ); + try { + // Force stop of the socket even if it had a connection map + // This is because we will be stopping this `QUICServer` which + // which will stop all the relevant connections + await this.socket.stop({ force: true }); + } catch (e) { + const e_ = new errors.ErrorQUICServerInternal( + 'Failed to stop QUICSocket', + { cause: e }, + ); + this.dispatchEvent(new events.EventQUICServerError({ detail: e_ })); + } + } + } + this._closed = true; + this.resolveClosedP(); + if (this[running] && this[status] !== 'stopping') { + if (error !== undefined) { + await this.stop({ + isApp: false, + errorCode: ConnectionErrorCode.InternalError, + reason: Buffer.from(error.description), + force: true, + }); + } else { + await this.stop({ force: true }); + } + } + }; + + /** + * Handles all `EventQUICSocket` events. + * Registered only if the socket is encapsulated. + */ + protected handleEventQUICSocket = (evt: EventAll) => { + if (evt.detail instanceof AbstractEvent) { + this.dispatchEvent(evt.detail.clone()); + } + }; + + /** + * Handles `EventQUICSocketStopped`. + * Registered once. + * + * It is an error if the socket was stopped while `QUICServer` wasn't + * stopped. + */ + protected handleEventQUICSocketStopped = () => { + const e = new errors.ErrorQUICServerSocketNotRunning(); + this.removeEventListener(EventAll.name, this.handleEventQUICSocket); + this.dispatchEvent( + new events.EventQUICServerError({ + detail: e, + }), + ); + }; + + /** + * Handles all `EventQUICConnection` events. + */ + protected handleEventQUICConnection = (evt: EventAll) => { + if (evt.detail instanceof AbstractEvent) { + this.dispatchEvent(evt.detail.clone()); + } + }; + + /** + * Handles `EventQUICConnectionSend`. + * + * This will propagate the connection send buffers to the socket. + * This may be concurrent and multiple send events may be processed + * at a time. + */ + protected handleEventQUICConnectionSend = async ( + evt: events.EventQUICConnectionSend, + ) => { + // We want to skip this if the socket has already ended + if (!(this.socket[running] && this.socket[status] !== 'stopping')) return; + try { + await this.socket.send_( + evt.detail.msg, + evt.detail.port, + evt.detail.address, ); - } else if (event instanceof events.QUICConnectionStopEvent) { - this.dispatchEvent(new events.QUICConnectionStopEvent()); - } else if (event instanceof events.QUICConnectionStreamEvent) { - this.dispatchEvent( - new events.QUICConnectionStreamEvent({ detail: event.detail }), + } catch (e) { + const e_ = new errors.ErrorQUICServerInternal( + 'Failed to send data on the QUICSocket', + { + data: evt.detail, + cause: e, + }, ); - } else if (event instanceof events.QUICStreamDestroyEvent) { - this.dispatchEvent(new events.QUICStreamDestroyEvent()); - } else { - utils.never(); + this.dispatchEvent(new events.EventQUICServerError({ detail: e_ })); } }; + /** + * Handles `EventQUICConnectionStopped`. + * Registered once. + */ + protected handleEventQUICConnectionStopped = ( + evt: events.EventQUICConnectionStopped, + ) => { + const quicConnection = evt.target as QUICConnection; + quicConnection.removeEventListener( + events.EventQUICConnectionSend.name, + this.handleEventQUICConnectionSend, + ); + quicConnection.removeEventListener( + EventAll.name, + this.handleEventQUICConnection, + ); + this.socket.connectionMap.delete(quicConnection.connectionId); + }; + + /** + * Constructs a QUIC server. + * + * @param opts + * @param opts.crypto - server needs to be able to sign and verify symmetrically. + * @param opts.config - defaults to `serverDefault` + * @param opts.socket - optional shared QUICSocket + * @param opts.resolveHostname - defaults to using OS DNS resolver + * @param opts.reasonToCode - maps stream error reasons to stream error codes + * @param opts.codeToReason - maps stream error codes to reasons + * @param opts.minIdleTimeout - can be set to override the starting timeout + * for accepted connections + * @param opts.logger + */ + public constructor(opts: { + crypto: QUICServerCrypto; + config: QUICServerConfigInput; + resolveHostname?: ResolveHostname; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + minIdleTimeout?: number; + logger?: Logger; + }); + public constructor(opts: { + crypto: QUICServerCrypto; + config: QUICServerConfigInput; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + minIdleTimeout?: number; + logger?: Logger; + }); public constructor({ crypto, config, @@ -109,31 +260,18 @@ class QUICServer extends EventTarget { resolveHostname = utils.resolveHostname, reasonToCode, codeToReason, - verifyCallback, minIdleTimeout, logger, }: { - crypto: { - key: ArrayBuffer; - ops: ServerCrypto; - }; - config: Partial & { - key: string | Array | Uint8Array | Array; - cert: string | Array | Uint8Array | Array; - }; + crypto: QUICServerCrypto; + config: QUICServerConfigInput; socket?: QUICSocket; - resolveHostname?: (hostname: string) => Host | PromiseLike; + resolveHostname?: ResolveHostname; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - verifyCallback?: VerifyCallback; minIdleTimeout?: number; logger?: Logger; }) { - super(); - const quicConfig = { - ...serverDefault, - ...config, - }; this.logger = logger ?? new Logger(this.constructor.name); this.crypto = crypto; if (socket == null) { @@ -146,32 +284,47 @@ class QUICServer extends EventTarget { this.socket = socket; this.isSocketShared = true; } - // Registers itself to the socket - this.socket.registerServer(this); - // Shares the socket connection map as well - this.connectionMap = this.socket.connectionMap; - this.config = quicConfig; + this.config = { + ...serverDefault, + ...config, + }; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; - this.verifyCallback = verifyCallback; this.minIdleTimeout = minIdleTimeout; + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); + this._closedP = closedP; + this.resolveClosedP = resolveClosedP; } @ready(new errors.ErrorQUICServerNotRunning()) - public get host(): string { + public get host(): Host { return this.socket.host; } @ready(new errors.ErrorQUICServerNotRunning()) - public get port(): number { + public get port(): Port { return this.socket.port; } /** - * Starts the QUICServer + * Server is no longer accepting connections. + */ + public get closed() { + return this._closed; + } + + public get closedP(): Promise { + return this._closedP; + } + + /** + * Starts the QUICServer. * - * If the QUIC socket is shared, then it is expected that it is already started. - * In which case, the `host` and `port` parameters here are ignored. + * @param opts + * @param opts.host - target host, ignored if socket is shared + * @param opts.port - target port, ignored if socket is shared + * @param opts.reuseAddr - reuse existing port + * @param opts.ipv6Only - force using IPv6 even when using `::` */ public async start({ host = '::', @@ -187,7 +340,6 @@ class QUICServer extends EventTarget { let address: string; if (!this.isSocketShared) { address = utils.buildAddress(host, port); - this.address = address; this.logger.info(`Start ${this.constructor.name} on ${address}`); await this.socket.start({ host, port, reuseAddr, ipv6Only }); address = utils.buildAddress(this.socket.host, this.socket.port); @@ -199,57 +351,122 @@ class QUICServer extends EventTarget { address = utils.buildAddress(this.socket.host, this.socket.port); this.logger.info(`Start ${this.constructor.name} on ${address}`); } - // Register on all socket events - this.socket.addEventListener('socketError', this.handleQUICSocketEvents); - this.socket.addEventListener('socketStop', this.handleQUICSocketEvents); + this.socket.setServer(this); + this.addEventListener( + events.EventQUICServerError.name, + this.handleEventQUICServerError, + ); + this.addEventListener( + events.EventQUICServerClose.name, + this.handleEventQUICServerClose, + { once: true }, + ); + this.socket.addEventListener( + events.EventQUICSocketStopped.name, + this.handleEventQUICSocketStopped, + { once: true }, + ); + if (!this.isSocketShared) { + this.socket.addEventListener(EventAll.name, this.handleEventQUICSocket); + } + this._closed = false; this.logger.info(`Started ${this.constructor.name} on ${address}`); } /** - * Stops the QUICServer + * Stops the QUICServer. + * + * @param opts + * @param opts.isApp - whether the stop is initiated by the application + * @param opts.errorCode - the error code to send to the peers + * @param opts.reason - the reason to send to the peers + * @param opts.force - force controls whether to cancel streams or wait for + * streams to close gracefully */ public async stop({ - force = false, - }: { - force?: boolean; - } = {}) { - const address = this.address; - this.logger.info(`Stop ${this.constructor.name} on ${address}`); - const destroyProms: Array> = []; - for (const connection of this.connectionMap.serverConnections.values()) { - destroyProms.push( + isApp = true, + errorCode = 0, + reason = new Uint8Array(), + force = true, + }: + | { + isApp: false; + errorCode?: ConnectionErrorCode; + reason?: Uint8Array; + force?: boolean; + } + | { + isApp?: true; + errorCode?: number; + reason?: Uint8Array; + force?: boolean; + } = {}) { + let address: string | undefined; + if (this.socket[running]) { + address = utils.buildAddress(this.socket.host, this.socket.port); + } + this.logger.info( + `Stop ${this.constructor.name}${address != null ? ` on ${address}` : ''}`, + ); + // Stop answering new connections + this.socket.unsetServer(); + const connectionsDestroyP: Array> = []; + for (const connection of this.socket.connectionMap.serverConnections.values()) { + connectionsDestroyP.push( connection.stop({ - applicationError: true, - errorMessage: 'cleaning up connections', + isApp, + errorCode, + reason, force, }), ); } - this.logger.debug('Awaiting connections to destroy'); - await Promise.all(destroyProms); - this.logger.debug('All connections destroyed'); - this.socket.deregisterServer(this); + await Promise.all(connectionsDestroyP); + if (!this._closed) { + this.dispatchEvent(new events.EventQUICServerClose()); + } + // Wait for the socket to be closed + await this._closedP; + // Resets the `closedP` + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); + this._closedP = closedP; + this.resolveClosedP = resolveClosedP; + this.removeEventListener( + events.EventQUICServerError.name, + this.handleEventQUICServerError, + ); + this.removeEventListener( + events.EventQUICServerClose.name, + this.handleEventQUICServerClose, + ); + // The socket may not have been stopped if it is shared + // In which case we just remove our listener here + this.socket.removeEventListener( + events.EventQUICSocketStopped.name, + this.handleEventQUICSocketStopped, + ); if (!this.isSocketShared) { - // If the socket is not shared, then it can be stopped - await this.socket.stop(); + this.socket.removeEventListener( + EventAll.name, + this.handleEventQUICSocket, + ); } - this.socket.removeEventListener('socketError', this.handleQUICSocketEvents); - this.socket.removeEventListener('socketStop', this.handleQUICSocketEvents); - this.dispatchEvent(new events.QUICServerStopEvent()); - this.logger.info(`Stopped ${this.constructor.name} on ${address}`); + this.logger.info( + `Stopped ${this.constructor.name}${ + address != null ? ` on ${address}` : '' + }`, + ); } - // Because the `ctx` is not passed in from the outside - // It makes sense that this is only done during construction - // And importantly we just enable the cancellation of this - // Nothing else really - /** - * This method must not throw any exceptions. - * Any errors must be emitted as events. + * Accepts new connection from the socket. + * + * This performs the new connection handshake. + * * @internal */ - public async connectionNew( + @ready(new errors.ErrorQUICServerNotRunning()) + public async acceptConnection( remoteInfo: RemoteInfo, header: Header, dcid: QUICConnectionId, @@ -263,30 +480,23 @@ class QUICServer extends EventTarget { ) { return; } - // Derive the new connection's SCID from the client generated DCID const scid = new QUICConnectionId( await this.crypto.ops.sign(this.crypto.key, dcid), 0, quiche.MAX_CONN_ID_LEN, ); - const peerAddress = utils.buildAddress(remoteInfo.host, remoteInfo.port); - // Version Negotiation if (!quiche.versionIsSupported(header.version)) { - this.logger.debug( - `QUIC packet version is not supported, performing version negotiation`, - ); const versionDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const versionDatagramLength = quiche.negotiateVersion( header.scid, header.dcid, versionDatagram, ); - this.logger.debug(`Send VersionNegotiation packet to ${peerAddress}`); try { - await this.socket.send( + await this.socket.send_( versionDatagram, 0, versionDatagramLength, @@ -294,10 +504,13 @@ class QUICServer extends EventTarget { remoteInfo.host, ); } catch (e) { - this.dispatchEvent(new events.QUICServerErrorEvent({ detail: e })); - return; + // This is a caller error + // Not a domain error for QUICServer + throw new errors.ErrorQUICServerNewConnection( + `Failed to send version datagram ${peerAddress}`, + { cause: e }, + ); } - this.logger.debug(`Sent VersionNegotiation packet to ${peerAddress}`); return; } // At this point we are processing an `Initial` packet. @@ -316,9 +529,8 @@ class QUICServer extends EventTarget { header.version, retryDatagram, ); - this.logger.debug(`Send Retry packet to ${peerAddress}`); try { - await this.socket.send( + await this.socket.send_( retryDatagram, 0, retryDatagramLength, @@ -326,10 +538,11 @@ class QUICServer extends EventTarget { remoteInfo.host, ); } catch (e) { - this.dispatchEvent(new events.QUICServerErrorEvent({ detail: e })); - return; + throw new errors.ErrorQUICServerNewConnection( + `Failed to send stateless retry datagram to ${peerAddress}`, + { cause: e }, + ); } - this.logger.debug(`Sent Retry packet to ${peerAddress}`); return; } // At this point in time, the packet's DCID is the originally-derived DCID. @@ -339,104 +552,91 @@ class QUICServer extends EventTarget { remoteInfo.host, ); if (dcidOriginal == null) { - this.logger.debug( - `QUIC packet token failed validation due to missing DCID`, - ); + // Failed validation due to missing DCID return; } // Check that the newly-derived DCID (passed in as the SCID) is the same // length as the packet DCID. // This ensures that the derivation process hasn't changed. if (scid.byteLength !== header.dcid.byteLength) { - this.logger.debug( - `QUIC packet token failed validation due to mismatched length`, - ); + // Failed validation due to mismatched length return; } // Here we shall re-use the originally-derived DCID as the SCID const newScid = new QUICConnectionId(header.dcid); - this.logger.debug( - `Accepting new connection from QUIC packet from ${remoteInfo.host}:${remoteInfo.port}`, + // Construct a QUIC connection that isn't yet started + const connection = new QUICConnection({ + type: 'server', + scid: newScid, + dcid: dcidOriginal, + socket: this.socket, + remoteInfo, + config: { ...this.config }, // Config must be copied in case it is updated + reasonToCode: this.reasonToCode, + codeToReason: this.codeToReason, + logger: this.logger.getChild(`${QUICConnection.name} ${scid.toString()}`), + }); + // This unstarted connection is set to the connection map which allows + // concurrent received packets to trigger the `recv` and `send` pair. + this.socket.connectionMap.set(connection.connectionId, connection); + connection.addEventListener( + events.EventQUICConnectionSend.name, + this.handleEventQUICConnectionSend, + ); + connection.addEventListener( + events.EventQUICConnectionStopped.name, + this.handleEventQUICConnectionStopped, + { once: true }, ); - const clientConnRef = Buffer.from(header.scid).toString('hex').slice(32); - let connection: QUICConnection; + connection.addEventListener(EventAll.name, this.handleEventQUICConnection); try { - connection = await QUICConnection.createQUICConnection( + await connection.start( { - type: 'server', - scid: newScid, - dcid: dcidOriginal, - socket: this.socket, - remoteInfo, data, - config: this.config, - reasonToCode: this.reasonToCode, - codeToReason: this.codeToReason, - verifyCallback: this.verifyCallback, - logger: this.logger.getChild( - `${QUICConnection.name} ${scid - .toString() - .slice(32)}-${clientConnRef}`, - ), + remoteInfo, }, { timer: this.minIdleTimeout }, ); } catch (e) { - // Ignoring any errors here as a failure to connect - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICServerConnectionFailed(undefined, { - cause: e, - }), - }), + connection.removeEventListener( + events.EventQUICConnectionSend.name, + this.handleEventQUICConnectionSend, + ); + connection.removeEventListener( + events.EventQUICConnectionStopped.name, + this.handleEventQUICConnectionStopped, + ); + connection.removeEventListener( + EventAll.name, + this.handleEventQUICConnection, + ); + this.socket.connectionMap.delete(connection.connectionId); + // This could be due to a runtime IO exception or start timeout + throw new errors.ErrorQUICServerNewConnection( + 'Failed to start accepted connection', + { cause: e }, ); - return; } - // Handling connection events - connection.addEventListener( - 'connectionError', - this.handleQUICConnectionEvents, - ); - connection.addEventListener( - 'connectionStream', - this.handleQUICConnectionEvents, - ); - connection.addEventListener( - 'streamDestroy', - this.handleQUICConnectionEvents, - ); - connection.addEventListener( - 'connectionStop', - (event) => { - connection.removeEventListener( - 'connectionError', - this.handleQUICConnectionEvents, - ); - connection.removeEventListener( - 'connectionStream', - this.handleQUICConnectionEvents, - ); - connection.removeEventListener( - 'streamDestroy', - this.handleQUICConnectionEvents, - ); - this.handleQUICConnectionEvents(event); - }, - { once: true }, - ); + // This connection is now started and ready to be used this.dispatchEvent( - new events.QUICServerConnectionEvent({ detail: connection }), + new events.EventQUICServerConnection({ detail: connection }), ); - return connection; } + public updateCrypto(crypto: Partial): void { + this.crypto = { + ...this.crypto, + ...crypto, + }; + } + /** - * This updates the `QUICConfig` used when new connections are established. - * Only the parameters that are provided are updated. - * It will not affect existing connections, they will keep using the old `QUICConfig` + * Updates the `QUICConfig` for new connections. + * It will not affect existing connections, they will keep using the old + * `QUICConfig`. */ - public updateConfig(config: Partial): void { + public updateConfig(config: Partial): void { this.config = { ...this.config, ...config, diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index d10bf61f..875a770e 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -1,48 +1,76 @@ import type QUICServer from './QUICServer'; -import type QUICConnection from './QUICConnection'; -import type { Host, Hostname, Port } from './types'; +import type { Host, Hostname, Port, ResolveHostname } from './types'; import type { Header } from './native/types'; import dgram from 'dgram'; import Logger from '@matrixai/logger'; -import { running } from '@matrixai/async-init'; -import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; -import { RWLockWriter } from '@matrixai/async-locks'; -import { status } from '@matrixai/async-init/dist/utils'; -import { withF } from '@matrixai/resources'; -import { utils as contextsUtils } from '@matrixai/contexts'; +import { StartStop, ready, running } from '@matrixai/async-init/dist/StartStop'; +import { utils as errorsUtils } from '@matrixai/errors'; import QUICConnectionId from './QUICConnectionId'; import QUICConnectionMap from './QUICConnectionMap'; import { quiche } from './native'; -import * as events from './events'; import * as utils from './utils'; +import * as events from './events'; import * as errors from './errors'; -/** - * Events: - * - socketError - * - socketStop - */ interface QUICSocket extends StartStop {} -@StartStop() -class QUICSocket extends EventTarget { +@StartStop({ + eventStart: events.EventQUICSocketStart, + eventStarted: events.EventQUICSocketStarted, + eventStop: events.EventQUICSocketStop, + eventStopped: events.EventQUICSocketStopped, +}) +class QUICSocket { + /** + * The connection map is defined here so that it can be shared between + * the `QUICClient` and the `QUICServer`. However every connection's + * lifecycle is managed by either the `QUICClient` or `QUICServer`. + * `QUICSocket` will not set or unset any connections in this connection map. + * @internal + */ public connectionMap: QUICConnectionMap = new QUICConnectionMap(); - protected socket: dgram.Socket; - protected _host: Host; - protected _port: Port; - protected _type: 'ipv4' | 'ipv6' | 'ipv4&ipv6'; - protected logger: Logger; + + /** + * Registered server for this socket. + * If a server is not registered for this socket, all packets for new + * connections will be dropped. + */ protected server?: QUICServer; - protected resolveHostname: (hostname: string) => Host | PromiseLike; + /** + * Hostname resolver. + */ + protected resolveHostname: ResolveHostname; + protected _host: Host; + protected _port: Port; + protected _type: 'ipv4' | 'ipv6' | 'ipv4&ipv6'; + protected _closed: boolean = false; + protected _closedP: Promise; + protected resolveClosedP: () => void; + protected socket: dgram.Socket; protected socketBind: (port: number, host: string) => Promise; protected socketClose: () => Promise; protected socketSend: (...params: Array) => Promise; + protected handleEventQUICSocketError = (evt: events.EventQUICSocketError) => { + const error = evt.detail; + this.logger.error(utils.formatError(error)); + }; + + protected handleEventQUICSocketClose = async () => { + await this.socketClose(); + this._closed = true; + this.resolveClosedP(); + if (this[running]) { + await this.stop({ force: true }); + } + }; + /** - * Handle the datagram from UDP socket + * Handles UDP socket message. + * * The `data` buffer could be multiple coalesced QUIC packets. * It could also be a non-QUIC packet data. * If it is non-QUIC, we can discard the data. @@ -63,13 +91,13 @@ class QUICSocket extends EventTarget { } catch (e) { // `BufferTooShort` and `InvalidPacket` means that this is not a QUIC // packet. If so, then we just ignore the packet. - if (e.message !== 'BufferTooShort' && e.message !== 'InvalidPacket') { - // Emit error if it is not a `BufferTooShort` or `InvalidPacket` error. - // This would indicate something went wrong in header parsing. - // This is not a critical error, but should be checked. - this.dispatchEvent(new events.QUICSocketErrorEvent({ detail: e })); + if (e.message === 'BufferTooShort' || e.message === 'InvalidPacket') { + return; } - return; + // If the error is niether `BufferTooShort` or `InvalidPacket`, this + // may indicate something went wrong in the header parsing, which should + // be a software error. + throw e; } // All QUIC packets will have the `dcid` header property // However short packets will not have the `scid` property @@ -79,71 +107,76 @@ class QUICSocket extends EventTarget { host: remoteInfo.address as Host, port: remoteInfo.port as Port, }; - let connection: QUICConnection; - if (!this.connectionMap.has(dcid)) { - // If the DCID is not known, and the server has not been registered then - // we discard the packet> + const connection = this.connectionMap.get(dcid); + if (connection != null) { + // In the QUIC protocol, acknowledging packets while in a draining + // state is optional. We can respond with `STATELESS_RESET` + // but it's not necessary, and ignoring is simpler + // https://www.rfc-editor.org/rfc/rfc9000.html#stateless-reset + await connection.recv(data, remoteInfo_); + } else { + // If the server is not registered, we cannot attempt to create a new + // connection for this packet. if (this.server == null) { return; } - // At this point, the connection may not yet be started - const connection_ = await this.server.connectionNew( - remoteInfo_, - header, - dcid, - data, - ); - // If there's no connection yet - // then the server is middle of version negotiation or stateless retry - if (connection_ == null) { - return; + try { + // This call will block until the connection is started which + // may require multiple `recv` and `send` pairs to process the + // received packets. + // In order to do this, firstly the initial `data` is faciliated by the + // `QUICServer`. And subsequently multiple `recv` and `send` pairs will + // occur concurrently while the the connection is starting. + // These concurrent `recv` and `send` pairs occur in this same handler, + // but just in the other branch of the current `if` statement where + // the connection object already exists in the connection map. + await this.server.acceptConnection(remoteInfo_, header, dcid, data); + } catch (e) { + if ( + errorsUtils.checkError(e, (e) => e instanceof errors.ErrorQUICSocket) + ) { + const e_ = new errors.ErrorQUICSocketInternal( + 'Failed to call accept connection due to socket send', + { cause: e }, + ); + this.dispatchEvent( + new events.EventQUICSocketError({ + detail: e_, + }), + ); + this.dispatchEvent(new events.EventQUICSocketClose()); + return; + } + // If the connection timed out during start, this is an expected + // possibility, because the remote peer might have become unavailable, + // in which case we can just ignore the error here. + if (e instanceof errors.ErrorQUICServerNewConnection) { + return; + } + throw e; } - connection = connection_; - } else { - connection = this.connectionMap.get(dcid)!; - } - // If the connection has already stopped running - // then we discard the packet. - if (!(connection[running] || connection[status] === 'starting')) { - return; - } - // Acquire the conn lock, this ensures mutual exclusion - // for state changes on the internal connection - try { - await withF( - [contextsUtils.monitor(connection.lockbox, RWLockWriter)], - async ([mon]) => { - await mon.withF(connection.lockCode, async (mon) => { - // Even if we are `stopping`, the `quiche` library says we need to - // continue processing any packets. - await connection.recv(data, remoteInfo_, mon); - await connection.send(mon); - }); - }, - ); - } catch (e) { - // Race condition with destroying socket, just ignore - if (!(e instanceof errors.ErrorQUICSocketNotRunning)) throw e; } }; /** - * Handle error on the DGRAM socket + * Constructs a QUIC socket. + * + * @param opts + * @param opts.resolveHostname - defaults to using OS DNS resolver + * @param opts.logger */ - protected handleSocketError = (e: Error) => { - this.dispatchEvent(new events.QUICSocketErrorEvent({ detail: e })); - }; - public constructor({ resolveHostname = utils.resolveHostname, logger, }: { - resolveHostname?: (hostname: string) => Host | PromiseLike; + resolveHostname?: ResolveHostname; logger?: Logger; }) { - super(); this.logger = logger ?? new Logger(this.constructor.name); this.resolveHostname = resolveHostname; + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); + this._closedP = closedP; + this.resolveClosedP = resolveClosedP; } /** @@ -154,8 +187,8 @@ class QUICSocket extends EventTarget { * Whereas `0.0.0.0` means only all IPv4. */ @ready(new errors.ErrorQUICSocketNotRunning()) - public get host(): string { - return utils.fromHost(this._host); + public get host(): Host { + return this._host; } /** @@ -164,8 +197,8 @@ class QUICSocket extends EventTarget { * Because `0` is always resolved to a specific port. */ @ready(new errors.ErrorQUICSocketNotRunning()) - public get port(): number { - return utils.fromPort(this._port); + public get port(): Port { + return this._port; } /** @@ -177,11 +210,30 @@ class QUICSocket extends EventTarget { return this._type; } + public get closed() { + return this._closed; + } + + public get closedP(): Promise { + return this._closedP; + } + /** - * Supports IPv4 and IPv6 addresses - * Note that if the host is `::`, this will also bind to `0.0.0.0`. - * The host and port here are the local host and port that the socket will bind to. - * If the host is a hostname such as `localhost`, this will perform do local resolution. + * Starts this QUICSocket. + * This supports hostnames and IPv4 and IPv6 addresses. + * If the host is `::`, this will also bind to `0.0.0.0`. + * + * @param opts + * @param opts.host - The host to bind to. Default is `::`. + * @param opts.port - The port to bind to. Default is `0`. + * @param opts.reuseAddr - Whether to reuse the address. Default is `false`. + * @param opts.ipv6Only - Whether to only bind to IPv6. Default is `false`. + * + * @throws {errors.ErrorQUICSocketInvalidBindAddress} If bind failed due to + * EINVAL or ENOTFOUND. EINVAL is due to using IPv4 host when creating a + * `udp6` socket. ENOTFOUND is when the hostname does not resolve + * or does not resolve to IPv6 when creating a `udp6` socket or does not + * resolve to IPv4 when creating a `udp4` socket. */ public async start({ host = '::', @@ -234,6 +286,10 @@ class QUICSocket extends EventTarget { ); } this.socket.removeListener('error', rejectErrorP); + // The dgram socket's error events might just be informational + // They don't necessarily correspond to an error + // Therefore we don't bother listening for it + // Unless we were propagating default events upwards const socketAddress = this.socket.address(); // This is the resolved IP, not the original hostname this._host = socketAddress.address as Host; @@ -247,16 +303,27 @@ class QUICSocket extends EventTarget { this._type = 'ipv6'; } this.socket.on('message', this.handleSocketMessage); - this.socket.on('error', this.handleSocketError); + this.addEventListener( + events.EventQUICSocketError.name, + this.handleEventQUICSocketError, + ); + this.addEventListener( + events.EventQUICSocketClose.name, + this.handleEventQUICSocketClose, + { once: true }, + ); + this._closed = false; address = utils.buildAddress(this._host, this._port); this.logger.info(`Started ${this.constructor.name} on ${address}`); } /** - * Will stop the socket. - * An `ErrorQUICSocketConnectionsActive` will be thrown if there are active connections. - * If force is true, it will skip checking connections and stop the socket. - * @param force - Will force the socket to end even if there are active connections, used for cleaning up after tests. + * Stop this QUICSocket. + * + * @param opts + * @param opts.force - Stop the socket even if the connection map is not empty. + * + * @throws {errors.ErrorQUICSocketConnectionsActive} */ public async stop({ force = false, @@ -268,17 +335,34 @@ class QUICSocket extends EventTarget { `Cannot stop QUICSocket with ${this.connectionMap.size} active connection(s)`, ); } + if (!this._closed) { + this.dispatchEvent(new events.EventQUICSocketClose()); + } + await this._closedP; + // Resets the `closedP` + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); + this._closedP = closedP; + this.resolveClosedP = resolveClosedP; + this.removeEventListener( + events.EventQUICSocketError.name, + this.handleEventQUICSocketError, + ); + this.removeEventListener( + events.EventQUICSocketClose.name, + this.handleEventQUICSocketClose, + ); this.socket.off('message', this.handleSocketMessage); - this.socket.off('error', this.handleSocketError); - await this.socketClose(); - this.dispatchEvent(new events.QUICSocketStopEvent()); this.logger.info(`Stopped ${this.constructor.name} on ${address}`); } /** - * Sends UDP datagram - * The UDP socket here is connectionless. - * The port and address are necessary. + * Sends UDP datagram. + * Because UDP socket is connectionless, the port and address are required. + * This call is used internally by the rest of the library, but it is not + * internal because it can be used for hole punching, which is an application + * concern. Therefore if this method throws an exception, it does necessarily + * mean that this `QUICSocket` is an error state. It could be the caller's + * fault. */ public async send( msg: string | Uint8Array | ReadonlyArray, @@ -305,71 +389,49 @@ class QUICSocket extends EventTarget { ); } const host = params[index] as Host | Hostname; - const [host_, udpType] = await utils.resolveHost( - host, - this.resolveHostname, + let [host_, udpType] = await utils.resolveHost(host, this.resolveHostname); + host_ = utils.validateTarget( + this._host, + this._type, + host_, + udpType, + errors.ErrorQUICSocketInvalidSendAddress, ); - if ( - this._type === 'ipv4' && - udpType !== 'udp4' && - !utils.isIPv4MappedIPv6(host_) - ) { - throw new errors.ErrorQUICSocketInvalidSendAddress( - `Cannot send to ${host_} on an IPv4 QUICSocket`, - ); - } else if ( - this._type === 'ipv6' && - (udpType !== 'udp6' || utils.isIPv4MappedIPv6(host_)) - ) { - throw new errors.ErrorQUICSocketInvalidSendAddress( - `Cannot send to ${host_} on an IPv6 QUICSocket`, - ); - } else if (this._type === 'ipv4&ipv6' && udpType !== 'udp6') { - throw new errors.ErrorQUICSocketInvalidSendAddress( - `Cannot send to ${host_} on a dual stack QUICSocket`, - ); - } else if ( - this._type === 'ipv4' && - utils.isIPv4MappedIPv6(this._host) && - !utils.isIPv4MappedIPv6(host_) - ) { - throw new errors.ErrorQUICSocketInvalidSendAddress( - `Cannot send to ${host_} an IPv4 mapped IPv6 QUICSocket`, - ); - } params[index] = host_; return this.socketSend(...params); } /** - * Sets a single server to the socket - * You can only have 1 server for the socket - * The socket message handling can dispatch new connections to the new server - * Consider it is an event... therefore a new connection - * Although that would be if there's an event being emitted - * One way is to make QUICSocket an EventTarget - * Then for server to add a handler to it, by doing addEventListener('connection', ...) - * Or something else - * But why bother with this pub/sub system - * Just go straight to calling a thing - * We can call this.server.handleConnection() - * Why `handleConnection` because technically it's built on top of the handleMessage - * That becomes the key idea there - * handleNewConnection - * And all sorts of other stuff! - * Or whatever it needs to be + * This is an internal send that is faster. + * It does not do any resolution or validation of the target. + * If one of the internal procedures in this library calls this method and it + * throws up a caller error, then it could be considered an internal error. + * There are no known intermittent runtime errors from sending UDP packets. + * @internal */ - public registerServer(server: QUICServer) { - if (this.server != null && this.server[running]) { - throw new errors.ErrorQUICSocketServerDuplicate(); - } + public async send_( + msg: string | Uint8Array | ReadonlyArray, + port: number, + address: string, + ): Promise; + public async send_( + msg: string | Uint8Array, + offset: number, + length: number, + port: number, + address: string, + ): Promise; + @ready(new errors.ErrorQUICSocketNotRunning()) + public async send_(...params: Array): Promise { + return this.socketSend(...params); + } + + public setServer(server: QUICServer) { this.server = server; } - public deregisterServer(server: QUICServer) { - if (this.server === server) { - delete this.server; - } + public unsetServer() { + delete this.server; } } diff --git a/src/QUICStream.ts b/src/QUICStream.ts index ca633551..1bf239ef 100644 --- a/src/QUICStream.ts +++ b/src/QUICStream.ts @@ -1,17 +1,16 @@ +import type { + ReadableWritablePair, + ReadableStreamDefaultController, + WritableStreamDefaultController, +} from 'stream/web'; import type QUICConnection from './QUICConnection'; import type { - QUICStreamMap, + QUICConfig, + ConnectionMetadata, StreamId, StreamReasonToCode, StreamCodeToReason, - ConnectionMetadata, } from './types'; -import type { Connection } from './native/types'; -import type { - ReadableWritablePair, - ReadableStreamDefaultController, - WritableStreamDefaultController, -} from 'stream/web'; import { ReadableStream, WritableStream, @@ -21,495 +20,858 @@ import Logger from '@matrixai/logger'; import { CreateDestroy, ready, + destroyed, status, } from '@matrixai/async-init/dist/CreateDestroy'; import { quiche } from './native'; -import * as events from './events'; import * as utils from './utils'; +import * as events from './events'; import * as errors from './errors'; -import { never } from './utils'; -/** - * Events: - * - streamDestroy - * - * Swap from using `readable` and `writable` to just function calls. - * It's basically the same, since it's just the connection telling the stream - * is readable/writable. Rather than creating events for it. - */ -interface QUICStream extends CreateDestroy {} -@CreateDestroy() -class QUICStream - extends EventTarget - implements ReadableWritablePair -{ - public streamId: StreamId; - public readable: ReadableStream; - public writable: WritableStream; - - protected logger: Logger; - protected connection: QUICConnection; - protected conn: Connection; - protected streamMap: QUICStreamMap; - protected reasonToCode: StreamReasonToCode; - protected codeToReason: StreamCodeToReason; - protected readableController: ReadableStreamDefaultController; - protected writableController: WritableStreamDefaultController; - protected _sendClosed: boolean = false; - protected _recvClosed: boolean = false; - protected resolveReadableP?: () => void; - protected resolveWritableP?: () => void; - protected destroyProm = utils.promise(); +const abortReadablePReason = Symbol('abort readableP reason'); +interface QUICStream extends CreateDestroy {} +@CreateDestroy({ + eventDestroy: events.EventQUICStreamDestroy, + eventDestroyed: events.EventQUICStreamDestroyed, +}) +class QUICStream implements ReadableWritablePair { /** - * For `reasonToCode`, return 0 means "unknown reason" - * It is the catch-all for codes. - * So it is the default reason. + * Creates a QUIC stream. + * + * This creation is synchronous as it avoids the need for concurrent locking + * for generating new stream IDs. + * + * @param opts + * @param opts.initiated - local or peer initiated stream + * @param opts.streamId - stream ID + * @param opts.connection - QUIC connection + * @param opts.config - QUIC config + * @param opts.reasonToCode - maps stream error reasons to stream error codes + * @param opts.codeToReason - maps stream error codes to reasons + * @param opts.logger * - * It may receive any reason for cancellation. - * It may receive an exception when streamRecv fails! + * The `reasonToCode` defaults to returning `0` as the code. + * The `codeToReason` defaults to returning `Error` instance. */ - public static async createQUICStream({ + public static createQUICStream({ + initiated, streamId, connection, + config, reasonToCode = () => 0, codeToReason = (type, code) => new Error(`${type.toString()} ${code.toString()}`), logger = new Logger(`${this.name} ${streamId}`), }: { + initiated: 'local' | 'peer'; streamId: StreamId; connection: QUICConnection; + config: QUICConfig; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; logger?: Logger; - }): Promise { + }): QUICStream { logger.info(`Create ${this.name}`); - // 'send' a 0-len message to initialize stream state in Quiche. No 0-len data is actually sent so this does not - // create Peer state. - try { - connection.conn.streamSend(streamId, new Uint8Array(0), false); - } catch (e) { - // We ignore any errors here, if this is a server side stream then state already exists. - // But it's possible for the stream to already be closed or have an error here. - // These errors will be handled by the QUICStream and not here. - } const stream = new this({ + initiated, streamId, connection, + config, reasonToCode, codeToReason, logger, }); - connection.streamMap.set(stream.streamId, stream); + if (stream.type === 'uni') { + if (initiated === 'local') { + // Readable is automatically closed if it is local and unidirectional + stream.readableController.close(); + stream._readClosed = true; + } else if (initiated === 'peer') { + // Writable is automatically closed if it is peer and unidirectional + // This voids the promise, because the stream is a dummy stream + // and there's no other way to close the writable stream + // Ignores errors in case writable were to be aborted before it is closed + void stream.writable.close().catch(() => {}); + stream._writeClosed = true; + } + } + stream.addEventListener( + events.EventQUICStreamError.name, + stream.handleEventQUICStreamError, + ); + stream.addEventListener( + events.EventQUICStreamCloseRead.name, + stream.handleEventQUICStreamCloseRead, + { once: true }, + ); + stream.addEventListener( + events.EventQUICStreamCloseWrite.name, + stream.handleEventQUICStreamCloseWrite, + { once: true }, + ); logger.info(`Created ${this.name}`); return stream; } + public readonly type: 'bidi' | 'uni'; + public readonly initiated: 'local' | 'peer'; + public readonly streamId: StreamId; + public readonly readable: ReadableStream; + public readonly writable: WritableStream; + public readonly closedP: Promise; + + protected logger: Logger; + protected connection: QUICConnection; + protected reasonToCode: StreamReasonToCode; + protected codeToReason: StreamCodeToReason; + protected readableController: ReadableStreamDefaultController; + protected writableController: WritableStreamDefaultController; + protected _readClosed: boolean = false; + protected _writeClosed: boolean = false; + protected readableChunk?: Buffer; + protected resolveReadableP?: () => void; + protected rejectReadableP?: (reason?: any) => void; + protected resolveWritableP?: () => void; + protected rejectWritableP?: (reason?: any) => void; + protected resolveClosedP: () => void; + + /** + * Handles `EventQUICStreamError`. + * + * This event propagates all errors relating to locally cancelling or aborting + * the readable or writable, or receiving a `RESET_STREAM` or `STOP_SENDING` + * on the readable or writable respectively. + * + * Internal errors will be thrown upwards to become an uncaught exception. + * + * @throws {errors.ErrorQUICStreamInternal} + */ + protected handleEventQUICStreamError = (evt: events.EventQUICStreamError) => { + const error = evt.detail; + this.logger.error(utils.formatError(error)); + if (error instanceof errors.ErrorQUICStreamInternal) { + throw error; + } + if ( + error instanceof errors.ErrorQUICStreamLocalRead || + error instanceof errors.ErrorQUICStreamPeerRead + ) { + this.dispatchEvent( + new events.EventQUICStreamCloseRead({ + detail: error, + }), + ); + } else if ( + error instanceof errors.ErrorQUICStreamLocalWrite || + error instanceof errors.ErrorQUICStreamPeerWrite + ) { + this.dispatchEvent( + new events.EventQUICStreamCloseWrite({ + detail: error, + }), + ); + } + }; + + /** + * Handles `EventQUICStreamCloseRead`. + * Registered once. + */ + protected handleEventQUICStreamCloseRead = async () => { + this._readClosed = true; + if (this._readClosed && this._writeClosed) { + this.resolveClosedP(); + if (!this[destroyed] && this[status] !== 'destroying') { + // By disabling force, we don't end up running cancel again + await this.destroy({ force: false }); + } + } + }; + + /** + * Handles `EventQUICStreamCloseWrite`. + * Registered once. + */ + protected handleEventQUICStreamCloseWrite = async () => { + this._writeClosed = true; + if (this._readClosed && this._writeClosed) { + this.resolveClosedP(); + if (!this[destroyed] && this[status] !== 'destroying') { + // By disabling force, we don't end up running cancel again + await this.destroy({ force: false }); + } + } + }; + public constructor({ + initiated, streamId, connection, + config, reasonToCode, codeToReason, logger, }: { + initiated: 'local' | 'peer'; streamId: StreamId; connection: QUICConnection; + config: QUICConfig; reasonToCode: StreamReasonToCode; codeToReason: StreamCodeToReason; logger: Logger; }) { - super(); + if (utils.isStreamBidirectional(streamId)) { + this.type = 'bidi'; + } else if (utils.isStreamUnidirectional(streamId)) { + this.type = 'uni'; + } this.logger = logger; + this.initiated = initiated; this.streamId = streamId; this.connection = connection; - this.conn = connection.conn; - this.streamMap = connection.streamMap; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; - - this.readable = new ReadableStream( - { - start: (controller) => { - this.readableController = controller; - }, - pull: async (controller) => { - // If nothing to read then we wait - if (!this.conn.streamReadable(this.streamId)) { - const readProm = utils.promise(); - this.resolveReadableP = readProm.resolveP; - this.logger.debug('readable waiting for more data'); - await readProm.p; - if (!this.conn.streamReadable(this.streamId)) { - // If there is nothing to read then we are tying up loose ends, - // do nothing and return. I don't think this will even happen though. - return; - } - this.logger.debug('readable resuming'); - } - - const buf = Buffer.alloc(1024); - let recvLength: number, fin: boolean; - // Read messages until buffer is empty - try { - [recvLength, fin] = this.conn.streamRecv(this.streamId, buf); - } catch (e) { - this.logger.debug(`Stream recv reported: error ${e.message}`); - // Done means there is no more data to read - if (!this._recvClosed && e.message !== 'Done') { - const reason = - (await this.processSendStreamError(e, 'recv')) ?? e; - // If it is `StreamReset(u64)` error, then the peer has closed - // the stream, and we are receiving the error code - // If it is not a `StreamReset(u64)`, then something else broke, - // and we need to propagate the error up and down the stream - controller.error(reason); - await this.closeRecv(true, reason); - // It is possible the stream was cancelled, let's check the writable state; - try { - this.conn.streamWritable(this.streamId, 0); - } catch (e) { - const match = e.message.match(/InvalidStreamState\((.+)\)/); - if (match == null) { - return never( - 'Errors besides [InvalidStreamState(StreamId)] are not expected here', - ); - } - this.writableController.error(reason); - } - } - return; - } - this.logger.debug(`stream read ${recvLength} bytes with fin(${fin})`); - // Check and drop if we're already closed or message is 0-length message - if (!this._recvClosed && recvLength > 0) { - controller.enqueue(buf.subarray(0, recvLength)); - } - // If fin is true, then that means, the stream is CLOSED - if (fin) { - await this.closeRecv(); - controller.close(); - } - }, - cancel: async (reason) => { - this.logger.debug(`readable aborted with [${reason.message}]`); - await this.closeRecv(true, reason); - }, - }, - new CountQueuingStrategy({ - // Allow 1 buffered message, so we can know when data is desired, and we can know when to un-pause. - highWaterMark: 1, - }), - ); - - this.writable = new WritableStream( - { - start: (controller) => { - this.writableController = controller; - }, - write: async (chunk: Uint8Array, controller) => { - await this.streamSend(chunk).catch((e) => controller.error(e)); - await this.connection.send(); + const { p: closedP, resolveP: resolveClosedP } = utils.promise(); + this.closedP = closedP; + this.resolveClosedP = resolveClosedP; + // This will setup the readable chunk buffer with the size set to the + // configured per-stream buffer size. Note that this doubles the memory + // usage of each stream due to maintaining both the Rust and JS buffers + if (this.type === 'uni') { + if (initiated === 'local') { + // We expect the readable stream to be closed + this.readableChunk = undefined; + } else if (initiated === 'peer') { + this.readableChunk = Buffer.allocUnsafe(config.initialMaxStreamDataUni); + } + } else if (this.type === 'bidi' && initiated === 'local') { + this.readableChunk = Buffer.allocUnsafe( + config.initialMaxStreamDataBidiLocal, + ); + } else if (this.type === 'bidi' && initiated === 'peer') { + this.readableChunk = Buffer.allocUnsafe( + config.initialMaxStreamDataBidiRemote, + ); + } + if (this.type === 'uni' && initiated === 'local') { + // This is just a dummy stream that will be auto-closed during creation + this.readable = new ReadableStream({ + start: this.readableStart.bind(this), + }); + } else { + this.readable = new ReadableStream( + { + start: this.readableStart.bind(this), + pull: this.readablePull.bind(this), + cancel: this.readableCancel.bind(this), }, - close: async () => { - // Gracefully ends the stream with a 0-length fin frame - this.logger.debug('sending fin frame'); - await this.streamSend(new Uint8Array(0), true); - // Close without error - await this.closeSend(); + // Allow 1 buffered 'message', Buffering is handled via quiche + new CountQueuingStrategy({ + highWaterMark: 1, + }), + ); + } + if (this.type === 'uni' && this.initiated === 'peer') { + // This is just a dummy stream that will be auto-closed during creation + this.writable = new WritableStream({ + start: this.writableStart.bind(this), + }); + } else { + this.writable = new WritableStream( + { + start: this.writableStart.bind(this), + write: this.writableWrite.bind(this), + close: this.writableClose.bind(this), + abort: this.writableAbort.bind(this), }, - abort: async (reason?: any) => { - // Forces the stream to immediately close with an error. Will trigger a `RESET_STREAM` frame to be sent to - // the peer. Any buffered data is discarded. - await this.closeSend(true, reason); + { + // Allow 1 buffered 'message', Buffering is handled via quiche + highWaterMark: 1, }, - }, - { - // Allow 1 buffered 'message', Buffering is handled via quiche - highWaterMark: 1, - }, - ); - } - - public get sendClosed(): boolean { - return this._sendClosed; - } - - public get recvClosed(): boolean { - return this._recvClosed; - } - - public get destroyedP() { - return this.destroyProm.p; + ); + // Initialise local state only when it is not uni-directional and peer initiated + try { + // Quiche stream state doesn't yet exist until data is either received + // or sent on the stream. However in this QUIC library, one may want to + // create a new stream to use. Therefore in order to maintain consistent + // closing behaviour, we can prime the stream state in quiche by sending + // a 0-length message. The data is not actually send to the peer. + connection.conn.streamSend(streamId, new Uint8Array(0), false); + } catch (e) { + // If the peer initally sent `RESET_STREAM`, and we constructed the + // `QUICStream`, then we cannot create local quiche stream state. + // We would get the `StreamStopped` exception here. If so, we can + // ignore. + if (utils.isStreamStopped(e) === false) { + throw new errors.ErrorQUICStreamInternal( + 'Failed to prime local stream state with a 0-length message', + { cause: e }, + ); + } + } + } } /** - * Connection information including hosts, ports and cert data. + * Returns true of the writable has closed. */ - @ready(new errors.ErrorQUICStreamDestroyed()) - public get remoteInfo(): ConnectionMetadata { - return { - localHost: this.connection.localHost, - localPort: this.connection.localPort, - remoteCertificates: this.connection.getRemoteCertsChain(), - remoteHost: this.connection.remoteHost, - remotePort: this.connection.remotePort, - }; + public get writeClosed(): boolean { + return this._writeClosed; } /** - * Duplicating `remoteInfo` functionality. - * This strictly exists to work with agnostic RPC stream interface. + * Returns true if the readable has closed. */ + public get readClosed(): boolean { + return this._readClosed; + } + + @ready(new errors.ErrorQUICStreamDestroyed()) public get meta(): ConnectionMetadata { - return this.remoteInfo; + return this.connection.meta(); + } + + public get closed() { + return this._readClosed && this._writeClosed; } /** - * This method can be arrived top-down or bottom-up: + * Destroy the QUIC stream. * - * 1. Top-down control flow - means explicit destruction from QUICConnection - * 2. Bottom-up control flow - means stream events from users of this stream + * @param opts + * @param opts.force - if true, this will cancel readable and abort writable. + * @param opts.reason - the reason to send to the peer, and if readable and + * writable is cancelled and aborted, then this will be + * the readable and writable error. * - * This will not wait for any transition events, It's either called when both - * directions have closed. Or when force closing the connection which does not - * require waiting. + * @throws {errors.ErrorQUICStreamInternal} - if cancel fails */ - public async destroy() { + public async destroy({ + force = true, + reason, + }: { + force?: boolean; + reason?: any; + } = {}) { this.logger.info(`Destroy ${this.constructor.name}`); - // Force close any open streams - this.cancel(new errors.ErrorQUICStreamClose()); - // Removing stream from the connection's stream map - this.streamMap.delete(this.streamId); - this.dispatchEvent(new events.QUICStreamDestroyEvent()); + if (force && !(this._readClosed && this._writeClosed)) { + this.cancel(reason); + } + // If force is false, this will wait for graceful close of both readable + // and writable. + await this.closedP; + this.removeEventListener( + events.EventQUICStreamError.name, + this.handleEventQUICStreamError, + ); + this.removeEventListener( + events.EventQUICStreamCloseRead.name, + this.handleEventQUICStreamCloseRead, + ); + this.removeEventListener( + events.EventQUICStreamCloseWrite.name, + this.handleEventQUICStreamCloseWrite, + ); this.logger.info(`Destroyed ${this.constructor.name}`); } /** - * Used to cancel the streams. This function is synchronous and will immediately close the stream and not await any - * response. + * Cancels the readable and aborts the writable. + * + * If streams have already closed or cancelled then this will do nothing. + * If the underlying quiche streams already closed then this will do nothing. + * + * Cancellation will occur in the background. + * + * @throws {errors.ErrorQUICStreamInternal} - if cancel fails */ public cancel(reason?: any): void { - reason = reason ?? new errors.ErrorQUICStreamCancel(); - if (!this._recvClosed) { - this.readableController.error(reason); - void this.closeRecv(true, reason); - } - if (!this._sendClosed) { - this.writableController.error(reason); - void this.closeSend(true, reason); - } + this.readableCancel(reason); + this.writableAbort(reason); } /** - * Called when stream is present in the `connection.readable` iterator - * Checks for certain close conditions when blocked and closes the web-stream. + * Called when stream is present in the `this.connection.conn.readable` iterator. + * + * If the quiche stream received `RESET_STREAM`, then this is processed as an + * error, and will drop all buffered data. All other cases will be processed + * gracefully. + * + * Note that this does not dispatch `EventQUICStreamSend` because + * `QUICConnection` will process the connection send automatically, as the + * origin of change here is from the `QUICConnection`. + * + * @throws {errors.ErrorQUICStreamInternal} + * @internal */ @ready(new errors.ErrorQUICStreamDestroyed(), false, ['destroying']) public read(): void { - // If we're readable then we need to un-pause the readable stream. - // We also need to check for an early end condition here. - this.logger.debug(`desired size ${this.readableController.desiredSize}`); - if (this.conn.streamFinished(this.streamId)) { - this.logger.debug( - 'stream is finished and readable, processing end condition', - ); - // If we're finished and read was called then we need to read out the last message - // to check if it's a fin frame or an error. - // This duplicates some of the pull logic for processing an error or a fin frame. - // No actual data is expected in this case. - const buf = Buffer.alloc(1024); - let fin: boolean; + // Stream is finished if due to `RESET_STREAM` or `fin` flag + if (this.connection.conn.streamFinished(this.streamId)) { + let result: [number, boolean] | null; try { - [, fin] = this.conn.streamRecv(this.streamId, buf); - if (fin) { - // Closing the readable stream - void this.closeRecv(); - this.readableController.close(); - } + result = this.connection.conn.streamRecv( + this.streamId, + this.readableChunk!, + ); } catch (e) { - if (e.message !== 'Done') { - this.logger.debug(`Stream recv reported: error ${e.message}`); - if (!this._recvClosed) { - // Close stream in background - void (async () => { - const reason = - (await this.processSendStreamError(e, 'recv')) ?? e; - this.readableController.error(reason); - await this.closeRecv(true, reason); - // It is possible the stream was cancelled, let's check the writable state; - try { - this.conn.streamWritable(this.streamId, 0); - } catch (e) { - const match = e.message.match(/InvalidStreamState\((.+)\)/); - if (match == null) { - return never( - 'Errors besides [InvalidStreamState(StreamId)] are not expected here', - ); - } - this.writableController.error(reason); - } - })(); - } + // If due to `RESET_STREAM` immediately cancel the readable and drop all buffers + let code: number | false; + if ((code = utils.isStreamReset(e)) !== false) { + const reason = this.codeToReason('read', code); + const e_ = new errors.ErrorQUICStreamPeerRead( + 'Peer reset the readable stream', + { + data: { code }, + cause: reason, + }, + ); + // This is idempotent and won't error even if it is already stopped + this.readableController.error(reason); + // This rejects the readableP if it exists + // The pull method may be blocked by `await readableP` + // When rejected, it will throw up the exception + // However because the stream is errored, then + // the exception has no effect, and any reads of this stream + // will simply return `{ value: undefined, done: true }` + this.rejectReadableP?.(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + return; + } else { + const e_ = new errors.ErrorQUICStreamInternal( + 'Failed `streamRecv` on the readable stream', + { cause: e }, + ); + this.readableController.error(e_); + this.rejectReadableP?.(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; } } - // Clean up the readable block so any waiting read can finish - if (this.resolveReadableP != null) this.resolveReadableP(); - } - // Check if the readable is waiting for data and resolve the block - if ( - this.readableController.desiredSize != null && - this.readableController.desiredSize > 0 - ) { - if (this.resolveReadableP != null) this.resolveReadableP(); + if (result === null) { + // This is an error, because this must be readable at this point + const e = new errors.ErrorQUICStreamInternal( + 'Failed `streamRecv` on the readable stream', + ); + this.readableController.error(e); + this.rejectReadableP?.(e); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e, + }), + ); + throw e; + } + // Close the readable gracefully + const [recvLength] = result; + if (recvLength > 0) { + this.readableController.enqueue( + this.readableChunk!.subarray(0, recvLength), + ); + } + this.readableController.close(); + this.dispatchEvent(new events.EventQUICStreamCloseRead()); + // Abort the `readablePull` since we have already processed the fin frame + this.rejectReadableP?.(abortReadablePReason); + return; } + // Resolve the read blocking promise if exists + // If already resolved, this is a noop + this.resolveReadableP?.(); } /** - * Internal push is converted to an external pull - * External system decides when to unblock + * Called when stream is present in the `this.connection.conn.writable` iterator. + * + * If the quiche stream received `STOP_SENDING`, then this is processed as an + * error, and will drop all buffered data. All other cases will be processed + * gracefully. + * + * Note that this does not dispatch `EventQUICStreamSend` because `QUICConnection` will + * process the connection send automatically, as the origin of change here is from the + * `QUICConnection`. + * + * @throws {errors.ErrorQUICStreamInternal} + * @internal */ @ready(new errors.ErrorQUICStreamDestroyed(), false, ['destroying']) public write(): void { + // Stream is aborted if due to `STOP_SENDING` try { - // Checking if the writable had an error - this.conn.streamWritable(this.streamId, 0); + this.connection.conn.streamCapacity(this.streamId); } catch (e) { - // If it threw an error, then the stream was closed with an error - // We need to attempt a write to trigger state change and remove stream from writable iterator - void this.streamSend(Buffer.from('dummy data'), true).catch(() => {}); - } - // Resolve the write blocking promise - if (this.resolveWritableP != null) { - this.resolveWritableP(); + let code: number | false; + if ((code = utils.isStreamStopped(e)) !== false) { + // Cleanup the underlying quiche stream state, otherwise the stream + // remains writable and we end up re-creating a `QUICStream`. + this.connection.conn.streamShutdown( + this.streamId, + quiche.Shutdown.Write, + code, + ); + const reason = this.codeToReason('write', code); + const e_ = new errors.ErrorQUICStreamPeerWrite( + 'Peer stopped the writable stream', + { + data: { code }, + cause: reason, + }, + ); + this.writableController.error(reason); + this.rejectWritableP?.(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + return; + } else { + const e_ = new errors.ErrorQUICStreamInternal( + 'Local stream writable could not `streamSend`', + { cause: e }, + ); + this.writableController.error(e_); + this.rejectWritableP?.(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; + } } + // Resolve the write blocking promise if exists + // If already resolved, this is a noop + this.resolveWritableP?.(); } - protected async streamSend(chunk: Uint8Array, fin = false): Promise { - // Check if we have capacity to send. Doing so will signal to quiche how many bytes are waiting and the stream will - // not become writable until there is room. So we can wait for the space before sending. - try { - // Checking if stream has capacity and wait for room. - if (!this.conn.streamWritable(this.streamId, chunk.byteLength)) { - this.logger.debug( - `stream does not have capacity for ${chunk.byteLength} bytes, waiting for capacity`, - ); - const { p: writableP, resolveP: resolveWritableP } = utils.promise(); - this.resolveWritableP = resolveWritableP; - await writableP; - } + protected readableStart(controller: ReadableStreamDefaultController): void { + this.readableController = controller; + } - const sentLength = this.conn.streamSend(this.streamId, chunk, fin); - // Since we are checking beforehand, we never not send the whole message - if (sentLength < chunk.byteLength) never(); - this.logger.debug(`stream wrote ${sentLength} bytes with fin(${fin})`); - } catch (e) { - // We can fail with an error. Likely a `StreamStopped(u64)` exception indicating the stream has - // failed in some way. We need to process the error and propagate it to the web-stream. - const reason = (await this.processSendStreamError(e, 'send')) ?? e; - await this.closeSend(true, reason); - // Throws the exception back to the writer - throw reason; - } + protected writableStart(controller: WritableStreamDefaultController): void { + this.writableController = controller; } /** - * This is called from events on the stream - * If `isError` is true, then it will terminate with a reason. - * The reason is converted to a code, and sent in a `STOP_SENDING` frame. + * Serialised by `ReadableStream`. + * + * @throws {errors.ErrorQUICStreamInternal} */ - protected async closeRecv( - isError: boolean = false, - reason?: any, - ): Promise { - if (isError) this.logger.debug(`recv closed with error ${reason.message}`); - // Further closes are NOPs - if (this._recvClosed) return; - this.logger.debug(`Close Recv`); - // Indicate that the receiving side is closed - this._recvClosed = true; - if (isError) { + protected async readablePull(): Promise { + // Block the pull if the quiche stream is not readable + if (!this.connection.conn.streamReadable(this.streamId)) { + const { + p: readableP, + resolveP: resolveReadableP, + rejectP: rejectReadableP, + } = utils.promise(); + this.resolveReadableP = resolveReadableP; + this.rejectReadableP = rejectReadableP; try { - const code = isError ? await this.reasonToCode('send', reason) : 0; - // This will send a `STOP_SENDING` frame with the code - // When the other peer sends, they will get a `StreamStopped(u64)` exception - this.conn.streamShutdown(this.streamId, quiche.Shutdown.Read, code); + await readableP; } catch (e) { - // Ignore if already shutdown - if (e.message !== 'Done') throw e; + // Abort this if `this.read` already processed `fin` + if (e === abortReadablePReason) return; + throw e; } - this.readableController.error(reason); } - // Background the send to avoid deadlock - void this.connection.send(); - if (this._recvClosed && this._sendClosed) { - // Only destroy if we are not already destroying - // and that both recv and send is closed - this.destroyProm.resolveP(); - if (this[status] !== 'destroying') void this.destroy(); + let result: [number, boolean] | null; + try { + result = this.connection.conn.streamRecv( + this.streamId, + this.readableChunk!, + ); + } catch (e) { + let code: number | false; + if ((code = utils.isStreamReset(e)) !== false) { + const reason = this.codeToReason('read', code); + const e_ = new errors.ErrorQUICStreamPeerRead( + 'Peer reset the readable stream', + { + data: { code }, + cause: reason, + }, + ); + this.readableController.error(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + // The pull doesn't need to throw it upwards, the controller.error + // already ensures errored state, and any read operation will end up + // throwing, but we do it here to be symmetric with write. + throw reason; + } else { + const e_ = new errors.ErrorQUICStreamInternal( + 'Failed `streamRecv` on the readable stream', + { cause: e }, + ); + this.readableController.error(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; + } + } + if (result === null) { + const e = new errors.ErrorQUICStreamInternal( + 'Failed `streamRecv` on the readable stream because it is not readable', + ); + this.readableController.error(e); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e, + }), + ); + throw e; + } + const [recvLength, fin] = result; + if (recvLength > 0) { + this.readableController.enqueue( + this.readableChunk!.subarray(0, recvLength), + ); + } + if (fin) { + // Reader will receive `{ value: undefined, done: true }` + this.readableController.close(); + // If fin is true, then that means, the stream is CLOSED + this.dispatchEvent(new events.EventQUICStreamCloseRead()); } - this.logger.debug(`Closed Recv`); + this.dispatchEvent(new events.EventQUICStreamSend()); + return; } /** - * This is called from events on the stream. - * Will trigger any error and clean up logic events. - * If `isError` is true, then it will terminate with a reason. - * The reason is converted to a code, and sent in a `RESET_STREAM` frame. + * Serialised with `this.writableClose` by `WritableStream`. + * + * @throws {errors.ErrorQUICStreamInternal} */ - protected async closeSend( - isError: boolean = false, - reason?: any, - ): Promise { - if (isError) this.logger.debug(`send closed with error ${reason.message}`); - // Further closes are NOPs - if (this._sendClosed) return; - this.logger.debug(`Close Send`); - // Indicate that the sending side is closed - this._sendClosed = true; - if (isError) { + protected async writableWrite(chunk: Uint8Array): Promise { + if (chunk.byteLength === 0) { + return; + } + let sentLength: number; + while (true) { try { - const code = await this.reasonToCode('send', reason); - // This will send a `RESET_STREAM` frame with the code - // When the other peer receives, they will get a `StreamReset(u64)` exception - this.conn.streamShutdown(this.streamId, quiche.Shutdown.Write, code); + const result = this.connection.conn.streamSend( + this.streamId, + chunk, + false, + ); + if (result === null) { + // This will trigger send, and also loop back to the top + sentLength = 0; + } else { + sentLength = result; + } } catch (e) { - // Ignore if already shutdown - if (e.message !== 'Done') throw e; + let code: number | false; + if ((code = utils.isStreamStopped(e)) !== false) { + const reason = this.codeToReason('write', code); + const e_ = new errors.ErrorQUICStreamPeerWrite( + 'Peer stopped the writable stream', + { + data: { code }, + cause: reason, + }, + ); + this.writableController.error(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw reason; + } else { + const e_ = new errors.ErrorQUICStreamInternal( + 'Local stream writable could not `streamSend`', + { cause: e }, + ); + this.writableController.error(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; + } } - this.writableController.error(reason); + this.dispatchEvent(new events.EventQUICStreamSend()); + // If sent length is less than the chunk length, then blocker the writer. + // The `this.writableP` will resolve when there's more capacity on the buffer. + if (sentLength < chunk.byteLength) { + chunk = chunk.subarray(sentLength, chunk.byteLength); + const { + p: writableP, + resolveP: resolveWritableP, + rejectP: rejectWritableP, + } = utils.promise(); + this.resolveWritableP = resolveWritableP; + this.rejectWritableP = rejectWritableP; + await writableP; + continue; + } + return; } - // Background the send to avoid deadlock - void this.connection.send(); - if (this._recvClosed && this._sendClosed) { - // Only destroy if we are not already destroying - // and that both recv and send is closed - this.destroyProm.resolveP(); - if (this[status] !== 'destroying') void this.destroy(); + } + + /** + * Serialised with `this.writableWrite` by `WritableStream`. + * + * @throws {errors.ErrorQUICStreamInternal} + */ + protected writableClose(): void { + try { + // This will not throw `Done` if the chunk is 0-length as it is here. + this.connection.conn.streamSend(this.streamId, new Uint8Array(0), true); + } catch (e) { + let code: number | false; + // If the stream is already reset, we cannot gracefully close + if ((code = utils.isStreamStopped(e)) !== false) { + const reason = this.codeToReason('write', code); + const e_ = new errors.ErrorQUICStreamPeerWrite( + 'Peer stopped the writable stream', + { + data: { code }, + cause: reason, + }, + ); + this.writableController.error(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + // This fails the `close`, however no matter what + // the writable stream is in a closed state. + throw reason; + } else { + // This could happen due to `InvalidStreamState` + const e_ = new errors.ErrorQUICStreamInternal( + 'Local stream writable could not `streamSend`', + { cause: e }, + ); + this.writableController.error(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; + } } - this.logger.debug(`Closed Send`); + this.dispatchEvent(new events.EventQUICStreamCloseWrite()); + this.dispatchEvent(new events.EventQUICStreamSend()); + return; } /** - * This will process any errors from a `streamSend` or `streamRecv`, extract the code and covert to a reason. - * Will return null if the error was not an expected stream ending error. + * `ReadableStream` ensures that this method is idempotent + * + * @throws {errors.ErrorQUICStreamInternal} */ - protected async processSendStreamError( - e: Error, - type: 'recv' | 'send', - ): Promise { - let match = - e.message.match(/StreamStopped\((.+)\)/) ?? - e.message.match(/StreamReset\((.+)\)/); - if (match != null) { - const code = parseInt(match[1]); - return await this.codeToReason(type, code); + protected readableCancel(reason?: any): void { + if (this._readClosed) return; + const code = this.reasonToCode('read', reason); + // Discards buffered readable data + try { + // The stream may have already received `RESET_STREAM`. + // In which case this would return `null`. + this.connection.conn.streamShutdown( + this.streamId, + quiche.Shutdown.Read, + code, + ); + } catch (e) { + const e_ = new errors.ErrorQUICStreamInternal( + 'Local stream readable could not be shutdown', + { cause: e }, + ); + this.readableController.error(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; } - match = e.message.match(/InvalidStreamState\((.+)\)/); - if (match != null) { - // `InvalidStreamState()` returns the stream ID and not any actual error code - return never('Should never reach an [InvalidState(StreamId)] error'); + const e = new errors.ErrorQUICStreamLocalRead( + 'Closing readable stream locally', + { + data: { code }, + cause: reason, + }, + ); + this.readableController.error(reason); + this.rejectReadableP?.(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e, + }), + ); + this.dispatchEvent(new events.EventQUICStreamSend()); + return; + } + + /** + * `WritableStream` ensures that this method is idempotent. + * + * @throws {errors.ErrorQUICStreamInternal} + */ + protected writableAbort(reason?: any): void { + if (this._writeClosed) return; + const code = this.reasonToCode('write', reason); + // Discards buffered writable data + try { + // The stream may have already received `STOP_SENDING`. + // In which case this would return `null`. + this.connection.conn.streamShutdown( + this.streamId, + quiche.Shutdown.Write, + code, + ); + } catch (e) { + const e_ = new errors.ErrorQUICStreamInternal( + 'Local stream writable could not be shutdown', + { cause: e }, + ); + this.writableController.error(e_); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e_, + }), + ); + throw e_; } - return null; + const e = new errors.ErrorQUICStreamLocalWrite( + 'Closing writable stream locally', + { + data: { code }, + cause: reason, + }, + ); + this.writableController.error(reason); + this.rejectWritableP?.(reason); + this.dispatchEvent( + new events.EventQUICStreamError({ + detail: e, + }), + ); + this.dispatchEvent(new events.EventQUICStreamSend()); + return; } } diff --git a/src/config.ts b/src/config.ts index 3d3d0619..81d4c57e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,7 @@ import type { QUICConfig } from './types'; import type { Config as QuicheConfig } from './native/types'; import { quiche } from './native'; +import * as utils from './utils'; import * as errors from './errors'; /** @@ -24,33 +25,38 @@ const sigalgs = [ ].join(':'); /** - * Usually we would create separate timeouts for connecting vs idling. + * Usually we would create separate timeouts for starting vs keep-alive. * Unfortunately quiche only has 1 config option that controls both. * And it is not possible to mutate this option after connecting. - * Therefore, this option is just a way to set a shorter connecting timeout + * Therefore, this option is just a way to set a shorter start timeout * compared to the idling timeout. - * If this is the larger than the `maxIdleTimeout` (remember that `0` is `Infinity`) for `maxIdleTimeout`, then this has no effect. - * This only has an effect if this is set to a number less than `maxIdleTimeout`. - * Thus, it is the "minimum boundary" of the timeout during connecting. - * While the `maxIdleTimeout` is still the "maximum boundary" during connecting. + * If this is the larger than the `maxIdleTimeout` (where `0` means `Infinity`), + * then this has no effect. This only has an effect if this is set to a number + * less than `maxIdleTimeout`. Thus, it is the "minimum boundary" of the + * timeout when starting. While the `maxIdleTimeout` is still the "maximum + * boundary" when starting. + * Both `minIdleTimeout` and `maxIdleTimeout` defaults to `Infinity` (where `0` + * means `Infinity` for `maxIdleTimeout`), thus by default connections will not + * timeout when starting or during keep-alive. */ const minIdleTimeout = Infinity; const clientDefault: QUICConfig = { sigalgs, verifyPeer: true, - verifyAllowFail: false, grease: true, keepAliveIntervalTime: undefined, maxIdleTimeout: 0, - maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 - maxSendUdpPayloadSize: quiche.MIN_CLIENT_INITIAL_LEN, // 1200, + maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 65527, but set to 1350 + maxSendUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 1200, but set to 1350 initialMaxData: 10 * 1024 * 1024, initialMaxStreamDataBidiLocal: 1 * 1024 * 1024, initialMaxStreamDataBidiRemote: 1 * 1024 * 1024, initialMaxStreamDataUni: 1 * 1024 * 1024, initialMaxStreamsBidi: 100, initialMaxStreamsUni: 100, + maxConnectionWindow: quiche.MAX_CONNECTION_WINDOW, + maxStreamWindow: quiche.MAX_STREAM_WINDOW, enableDgram: [false, 0, 0], disableActiveMigration: true, applicationProtos: ['quic'], @@ -60,27 +66,25 @@ const clientDefault: QUICConfig = { const serverDefault: QUICConfig = { sigalgs, verifyPeer: false, - verifyAllowFail: false, grease: true, keepAliveIntervalTime: undefined, maxIdleTimeout: 0, - maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 - maxSendUdpPayloadSize: quiche.MIN_CLIENT_INITIAL_LEN, // 1200 + maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 65527 + maxSendUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 1200, but set to 1350 initialMaxData: 10 * 1024 * 1024, initialMaxStreamDataBidiLocal: 1 * 1024 * 1024, initialMaxStreamDataBidiRemote: 1 * 1024 * 1024, initialMaxStreamDataUni: 1 * 1024 * 1024, initialMaxStreamsBidi: 100, initialMaxStreamsUni: 100, + maxConnectionWindow: quiche.MAX_CONNECTION_WINDOW, + maxStreamWindow: quiche.MAX_STREAM_WINDOW, enableDgram: [false, 0, 0], disableActiveMigration: true, applicationProtos: ['quic'], enableEarlyData: true, }; -const textDecoder = new TextDecoder('utf-8'); -const textEncoder = new TextEncoder(); - /** * Converts QUICConfig to QuicheConfig. * This does not use all the options of QUICConfig. @@ -107,65 +111,26 @@ function buildQuicheConfig(config: QUICConfig): QuicheConfig { // This is a concatenated CA certificates in PEM format let caPEMBuffer: Uint8Array | undefined; if (config.ca != null) { - let caPEMString = ''; - if (typeof config.ca === 'string') { - caPEMString = config.ca.trim() + '\n'; - } else if (config.ca instanceof Uint8Array) { - caPEMString = textDecoder.decode(config.ca).trim() + '\n'; - } else if (Array.isArray(config.ca)) { - for (const c of config.ca) { - if (typeof c === 'string') { - caPEMString += c.trim() + '\n'; - } else { - caPEMString += textDecoder.decode(c).trim() + '\n'; - } - } - } - caPEMBuffer = textEncoder.encode(caPEMString); + const caPEMBuffers = utils.collectPEMs(config.ca); + caPEMBuffer = utils.textEncoder.encode(caPEMBuffers.join('')); } - // This is an array of private keys in PEM format + // This is an array of private keys in PEM format as buffers let keyPEMBuffers: Array | undefined; if (config.key != null) { - const keyPEMs: Array = []; - if (typeof config.key === 'string') { - keyPEMs.push(config.key.trim() + '\n'); - } else if (config.key instanceof Uint8Array) { - keyPEMs.push(textDecoder.decode(config.key).trim() + '\n'); - } else if (Array.isArray(config.key)) { - for (const k of config.key) { - if (typeof k === 'string') { - keyPEMs.push(k.trim() + '\n'); - } else { - keyPEMs.push(textDecoder.decode(k).trim() + '\n'); - } - } - } - keyPEMBuffers = keyPEMs.map((k) => textEncoder.encode(k)); + const keyPEMs = utils.collectPEMs(config.key); + keyPEMBuffers = keyPEMs.map((k) => utils.textEncoder.encode(k)); } - // This is an array of certificate chains in PEM format + // This is an array of certificate chains in PEM format as buffers let certChainPEMBuffers: Array | undefined; if (config.cert != null) { - const certChainPEMs: Array = []; - if (typeof config.cert === 'string') { - certChainPEMs.push(config.cert.trim() + '\n'); - } else if (config.cert instanceof Uint8Array) { - certChainPEMs.push(textDecoder.decode(config.cert).trim() + '\n'); - } else if (Array.isArray(config.cert)) { - for (const c of config.cert) { - if (typeof c === 'string') { - certChainPEMs.push(c.trim() + '\n'); - } else { - certChainPEMs.push(textDecoder.decode(c).trim() + '\n'); - } - } - } - certChainPEMBuffers = certChainPEMs.map((c) => textEncoder.encode(c)); + const certPEMsChain = utils.collectPEMs(config.cert); + certChainPEMBuffers = certPEMsChain.map((c) => utils.textEncoder.encode(c)); } let quicheConfig: QuicheConfig; try { quicheConfig = quiche.Config.withBoringSslCtx( config.verifyPeer, - config.verifyAllowFail, + config.verifyCallback != null, caPEMBuffer, keyPEMBuffers, certChainPEMBuffers, diff --git a/src/errors.ts b/src/errors.ts index 89b1cf0f..0b2d066a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,5 @@ import type { POJO } from '@matrixai/errors'; +import type { ConnectionError, CryptoError } from './native'; import AbstractError from '@matrixai/errors/dist/AbstractError'; class ErrorQUIC extends AbstractError { @@ -25,10 +26,6 @@ class ErrorQUICSocketNotRunning extends ErrorQUICSocket { static description = 'QUIC Socket is not running'; } -class ErrorQUICSocketServerDuplicate extends ErrorQUICSocket { - static description = 'QUIC Socket already has a server that is running'; -} - class ErrorQUICSocketConnectionsActive extends ErrorQUICSocket { static description = 'QUIC Socket has active connections'; } @@ -41,6 +38,35 @@ class ErrorQUICSocketInvalidSendAddress extends ErrorQUICSocket { static description = 'QUIC Socket cannot send to the specified address'; } +class ErrorQUICSocketInternal extends ErrorQUICSocket { + static description = 'QUIC Socket internal error'; +} + +class ErrorQUICClient extends ErrorQUIC { + static description = 'QUIC Client error'; +} + +class ErrorQUICClientDestroyed extends ErrorQUICClient { + static description = 'QUIC Client is destroyed'; +} + +class ErrorQUICClientCreateTimeout extends ErrorQUICClient { + static description = 'QUIC Client create timeout'; +} + +class ErrorQUICClientSocketNotRunning extends ErrorQUICClient { + static description = + 'QUIC Client cannot be created with an unstarted shared QUIC socket'; +} + +class ErrorQUICClientInvalidHost extends ErrorQUICClient { + static description = 'QUIC Client cannot be created with the specified host'; +} + +class ErrorQUICClientInternal extends ErrorQUICClient { + static description = 'QUIC Client internal error'; +} + class ErrorQUICServer extends ErrorQUIC { static description = 'QUIC Server error'; } @@ -54,77 +80,118 @@ class ErrorQUICServerSocketNotRunning extends ErrorQUICServer { 'QUIC Server cannot start with an unstarted shared QUIC socket'; } -class ErrorQUICServerConnectionFailed extends ErrorQUICServer { - static description = 'QUIC server failed to create or accept a connection'; +class ErrorQUICServerNewConnection extends ErrorQUICServer { + static description = 'QUIC Server creating a new connection'; } -class ErrorQUICClient extends ErrorQUIC { - static description = 'QUIC Client error'; +class ErrorQUICServerInternal extends ErrorQUICServer { + static description = 'QUIC Server internal error'; } -class ErrorQUICClientCreateTimeOut extends ErrorQUICClient { - static description = 'QUICC Client create timeout'; +class ErrorQUICConnection extends ErrorQUIC { + static description = 'QUIC Connection error'; } -class ErrorQUICClientDestroyed extends ErrorQUICClient { - static description = 'QUIC Client is destroyed'; +class ErrorQUICConnectionNotRunning extends ErrorQUICConnection { + static description = 'QUIC Connection is not running'; } -class ErrorQUICClientSocketNotRunning extends ErrorQUICClient { +class ErrorQUICConnectionClosed extends ErrorQUICConnection { static description = - 'QUIC Client cannot be created with an unstarted shared QUIC socket'; + 'QUIC Connection cannot be restarted because it has already been closed'; } -class ErrorQUICClientInvalidHost extends ErrorQUICClient { - static description = 'QUIC Client cannot be created with the specified host'; +class ErrorQUICConnectionStartData extends ErrorQUIC { + static description = + 'QUIC Connection start requires data when it is a server connection'; } -class ErrorQUICConnection extends ErrorQUIC { - static description = 'QUIC Connection error'; +class ErrorQUICConnectionStartTimeout extends ErrorQUICConnection { + static description = 'QUIC Connection start timeout'; } -class ErrorQUICConnectionNotRunning extends ErrorQUICConnection { - static description = 'QUIC Connection is not running'; +class ErrorQUICConnectionConfigInvalid extends ErrorQUICConnection { + static description = 'QUIC connection invalid configuration'; } -class ErrorQUICConnectionStartTimeOut extends ErrorQUICConnection { - static description = 'QUIC Connection start timeout'; +class ErrorQUICConnectionLocal extends ErrorQUICConnection { + static description = 'QUIC Connection local error'; + declare data: POJO & ConnectionError; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & ConnectionError; + cause?: T; + }, + ) { + super(message, options); + } } -/** - * Quiche does not create a local or peer error during idle timeout. - */ -class ErrorQUICConnectionIdleTimeOut extends ErrorQUICConnection { - static description = 'QUIC Connection reached idle timeout'; -} - -/** - * These errors arise from the internal quiche connection. - * These can be local errors (as in the case of TLS verification failure). - * Or they can be remote errors. - * If the connection fails to verify the peer, it will be a local error. - * The error code might be 304. - * You may want a "cause" though? - * But it's not always a cause - * Plus it might be useless - * Note that the reason can be buffer. - * Which means it does not need to be a reason - * - * Note that TlsFail error codes are documented here: - * https://github.com/google/boringssl/blob/master/include/openssl/ssl.h - */ -class ErrorQUICConnectionInternal extends ErrorQUICConnection { - static description = 'QUIC Connection internal conn error'; - public declare data: { - type: 'local' | 'remote'; - isApp: boolean; - errorCode: number; - reason: Uint8Array; - } & POJO; +class ErrorQUICConnectionLocalTLS extends ErrorQUICConnectionLocal { + static description = 'QUIC Connection local TLS error'; + declare data: POJO & + ConnectionError & { + errorCode: CryptoError; + }; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & + ConnectionError & { + errorCode: CryptoError; + }; + cause?: T; + }, + ) { + super(message, options); + } } -class ErrorQUICConnectionInvalidConfig extends ErrorQUICConnection { - static description = 'QUIC connection invalid configuration'; +class ErrorQUICConnectionPeer extends ErrorQUICConnection { + static description = 'QUIC Connection peer error'; + declare data: POJO & ConnectionError; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & ConnectionError; + cause?: T; + }, + ) { + super(message, options); + } +} + +class ErrorQUICConnectionPeerTLS extends ErrorQUICConnectionLocal { + static description = 'QUIC Connection local TLS error'; + declare data: POJO & + ConnectionError & { + errorCode: CryptoError; + }; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & + ConnectionError & { + errorCode: CryptoError; + }; + cause?: T; + }, + ) { + super(message, options); + } +} + +class ErrorQUICConnectionIdleTimeout extends ErrorQUICConnection { + static description = 'QUIC Connection max idle timeout exhausted'; +} + +class ErrorQUICConnectionInternal extends ErrorQUICConnection { + static description = 'QUIC Connection internal error'; } class ErrorQUICStream extends ErrorQUIC { @@ -135,16 +202,76 @@ class ErrorQUICStreamDestroyed extends ErrorQUICStream { static description = 'QUIC Stream is destroyed'; } -class ErrorQUICStreamClose extends ErrorQUICStream { - static description = 'QUIC Stream force close'; +class ErrorQUICStreamLocalRead extends ErrorQUICStream { + static description = 'QUIC Stream locally closed readable side'; + declare data: POJO & { code: number }; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & { + code: number; + }; + cause?: T; + }, + ) { + super(message, options); + } +} + +class ErrorQUICStreamLocalWrite extends ErrorQUICStream { + static description = 'QUIC Stream locally closed writable side'; + declare data: POJO & { code: number }; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & { + code: number; + }; + cause?: T; + }, + ) { + super(message, options); + } } -class ErrorQUICStreamCancel extends ErrorQUICStream { - static description = 'QUIC Stream was cancelled without a provided reason'; +class ErrorQUICStreamPeerRead extends ErrorQUICStream { + static description = 'QUIC Stream peer closed readable side'; + declare data: POJO & { code: number }; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & { + code: number; + }; + cause?: T; + }, + ) { + super(message, options); + } } -class ErrorQUICUndefinedBehaviour extends ErrorQUIC { - static description = 'This should never happen'; +class ErrorQUICStreamPeerWrite extends ErrorQUICStream { + static description = 'QUIC Stream peer closed writable side'; + declare data: POJO & { code: number }; + constructor( + message: string = '', + options: { + timestamp?: Date; + data: POJO & { + code: number; + }; + cause?: T; + }, + ) { + super(message, options); + } +} + +class ErrorQUICStreamInternal extends ErrorQUICStream { + static description = 'QUIC Stream internal error'; } export { @@ -154,28 +281,38 @@ export { ErrorQUICConfig, ErrorQUICSocket, ErrorQUICSocketNotRunning, - ErrorQUICSocketServerDuplicate, ErrorQUICSocketConnectionsActive, ErrorQUICSocketInvalidBindAddress, ErrorQUICSocketInvalidSendAddress, - ErrorQUICServer, - ErrorQUICServerNotRunning, - ErrorQUICServerSocketNotRunning, - ErrorQUICServerConnectionFailed, + ErrorQUICSocketInternal, ErrorQUICClient, - ErrorQUICClientCreateTimeOut, ErrorQUICClientDestroyed, + ErrorQUICClientCreateTimeout, ErrorQUICClientSocketNotRunning, ErrorQUICClientInvalidHost, + ErrorQUICClientInternal, + ErrorQUICServer, + ErrorQUICServerNotRunning, + ErrorQUICServerSocketNotRunning, + ErrorQUICServerNewConnection, + ErrorQUICServerInternal, ErrorQUICConnection, ErrorQUICConnectionNotRunning, - ErrorQUICConnectionStartTimeOut, - ErrorQUICConnectionIdleTimeOut, + ErrorQUICConnectionClosed, + ErrorQUICConnectionStartData, + ErrorQUICConnectionStartTimeout, + ErrorQUICConnectionConfigInvalid, + ErrorQUICConnectionLocal, + ErrorQUICConnectionLocalTLS, + ErrorQUICConnectionPeer, + ErrorQUICConnectionPeerTLS, + ErrorQUICConnectionIdleTimeout, ErrorQUICConnectionInternal, - ErrorQUICConnectionInvalidConfig, ErrorQUICStream, ErrorQUICStreamDestroyed, - ErrorQUICStreamClose, - ErrorQUICStreamCancel, - ErrorQUICUndefinedBehaviour, + ErrorQUICStreamLocalRead, + ErrorQUICStreamLocalWrite, + ErrorQUICStreamPeerRead, + ErrorQUICStreamPeerWrite, + ErrorQUICStreamInternal, }; diff --git a/src/events.ts b/src/events.ts index 04434ecd..f19fae4c 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,157 +1,221 @@ import type QUICConnection from './QUICConnection'; import type QUICStream from './QUICStream'; +import type { + ErrorQUICConnectionLocal, + ErrorQUICConnectionPeer, + ErrorQUICConnectionInternal, + ErrorQUICStreamLocalRead, + ErrorQUICStreamLocalWrite, + ErrorQUICStreamPeerRead, + ErrorQUICStreamPeerWrite, + ErrorQUICStreamInternal, + ErrorQUICConnectionIdleTimeout, + ErrorQUICSocketInternal, + ErrorQUICServerInternal, + ErrorQUICServerSocketNotRunning, + ErrorQUICClientSocketNotRunning, + ErrorQUICClientInternal, +} from './errors'; +import { AbstractEvent } from '@matrixai/events'; + +abstract class EventQUIC extends AbstractEvent {} // Socket events -abstract class QUICSocketEvent extends Event {} - -class QUICSocketStartEvent extends Event { - constructor(options?: EventInit) { - super('socketStart', options); - } -} - -class QUICSocketStopEvent extends Event { - constructor(options?: EventInit) { - super('socketStop', options); - } -} - -class QUICSocketErrorEvent extends Event { - public detail: Error; - constructor( - options: EventInit & { - detail: Error; - }, - ) { - super('socketError', options); - this.detail = options.detail; - } -} +abstract class EventQUICSocket extends EventQUIC {} + +class EventQUICSocketStart extends EventQUICSocket {} + +class EventQUICSocketStarted extends EventQUICSocket {} + +class EventQUICSocketStop extends EventQUICSocket {} + +class EventQUICSocketStopped extends EventQUICSocket {} + +class EventQUICSocketError extends EventQUICSocket< + ErrorQUICSocketInternal +> {} + +class EventQUICSocketClose extends EventQUICSocket< + ErrorQUICSocketInternal | undefined +> {} // Client events -abstract class QUICClientEvent extends Event {} - -class QUICClientDestroyEvent extends Event { - constructor(options?: EventInit) { - super('clientDestroy', options); - } -} - -class QUICClientErrorEvent extends Event { - public detail: Error; - constructor( - options: EventInit & { - detail: Error; - }, - ) { - super('clientError', options); - this.detail = options.detail; - } -} +abstract class EventQUICClient extends EventQUIC {} + +class EventQUICClientDestroy extends EventQUICClient {} + +class EventQUICClientDestroyed extends EventQUICClient {} + +/** + * All `EventQUICConnectionError` errors is also `EventQUICClient` errors. + * This is because `QUICClient` is 1 to 1 to `QUICConnection`. + * It's thin wrapper around it. + */ +class EventQUICClientError extends EventQUICClient< + | ErrorQUICClientSocketNotRunning + | ErrorQUICClientInternal + | ErrorQUICConnectionLocal + | ErrorQUICConnectionPeer + | ErrorQUICConnectionIdleTimeout + | ErrorQUICConnectionInternal +> {} + +class EventQUICClientClose extends EventQUICClient< + | ErrorQUICClientSocketNotRunning + | ErrorQUICConnectionLocal + | ErrorQUICConnectionPeer + | ErrorQUICConnectionIdleTimeout +> {} // Server events -abstract class QUICServerEvent extends Event {} - -class QUICServerConnectionEvent extends Event { - public detail: QUICConnection; - constructor( - options: EventInit & { - detail: QUICConnection; - }, - ) { - super('serverConnection', options); - this.detail = options.detail; - } -} - -class QUICServerStartEvent extends Event { - constructor(options?: EventInit) { - super('serverStart', options); - } -} - -class QUICServerStopEvent extends Event { - constructor(options?: EventInit) { - super('serverStop', options); - } -} - -class QUICServerErrorEvent extends Event { - public detail: QUICSocketErrorEvent | Error; - constructor( - options: EventInit & { - detail: QUICSocketErrorEvent | Error; - }, - ) { - super('serverError', options); - this.detail = options.detail; - } -} +abstract class EventQUICServer extends EventQUIC {} + +class EventQUICServerConnection extends EventQUICServer {} + +class EventQUICServerStart extends EventQUICServer {} + +class EventQUICServerStarted extends EventQUICServer {} + +class EventQUICServerStop extends EventQUICServer {} + +class EventQUICServerStopped extends EventQUICServer {} + +class EventQUICServerError extends EventQUICServer< + ErrorQUICServerSocketNotRunning | ErrorQUICServerInternal +> {} + +class EventQUICServerClose extends EventQUICServer< + ErrorQUICServerSocketNotRunning | undefined +> {} // Connection events -abstract class QUICConnectionEvent extends Event {} - -class QUICConnectionStreamEvent extends QUICConnectionEvent { - public detail: QUICStream; - constructor( - options: EventInit & { - detail: QUICStream; - }, - ) { - super('connectionStream', options); - this.detail = options.detail; - } -} - -class QUICConnectionStopEvent extends QUICConnectionEvent { - constructor(options?: EventInit) { - super('connectionStop', options); - } -} - -class QUICConnectionErrorEvent extends QUICConnectionEvent { - public detail: Error; - constructor( - options: EventInit & { - detail: Error; - }, - ) { - super('connectionError', options); - this.detail = options.detail; - } -} +abstract class EventQUICConnection extends EventQUIC {} -// Stream events +class EventQUICConnectionStart extends EventQUICConnection {} + +class EventQUICConnectionStarted extends EventQUICConnection {} + +class EventQUICConnectionStop extends EventQUICConnection {} + +class EventQUICConnectionStopped extends EventQUICConnection {} -abstract class QUICStreamEvent extends Event {} +/** + * Closing a quic connection is always an error no matter if it is graceful or + * not. This is due to the utilisation of the error code and reason during + * connection close. Additionally it is also possible that that the QUIC + * connection times out. In this case, quiche does will not send a + * `CONNECTION_CLOSE` frame. + */ +class EventQUICConnectionError extends EventQUICConnection< + | ErrorQUICConnectionLocal + | ErrorQUICConnectionPeer + | ErrorQUICConnectionIdleTimeout + | ErrorQUICConnectionInternal +> {} + +class EventQUICConnectionClose extends EventQUICConnection< + | ErrorQUICConnectionLocal + | ErrorQUICConnectionPeer + | ErrorQUICConnectionIdleTimeout +> {} + +class EventQUICConnectionStream extends EventQUICConnection {} + +class EventQUICConnectionSend extends EventQUICConnection<{ + msg: Uint8Array; + port: number; + address: string; +}> {} + +// Stream events -class QUICStreamDestroyEvent extends QUICStreamEvent { - constructor(options?: EventInit) { - super('streamDestroy', options); - } -} +abstract class EventQUICStream extends EventQUIC {} + +class EventQUICStreamDestroy extends EventQUICStream {} + +class EventQUICStreamDestroyed extends EventQUICStream {} + +/** + * Gracefully closing a QUIC stream does not require an error event. + */ +class EventQUICStreamError extends EventQUICStream< + | ErrorQUICStreamLocalRead + | ErrorQUICStreamLocalWrite + | ErrorQUICStreamPeerRead + | ErrorQUICStreamPeerWrite + | ErrorQUICStreamInternal +> {} + +/** + * QUIC stream readable side is closed. + * + * `ErrorQUICStreamLocalRead` - readable side cancelled locally with code. + * `ErrorQUICStreamPeerRead` - readable side cancelled by peer aborting the + * remote writable side. + * `undefined` - readable side closed gracefully. + */ +class EventQUICStreamCloseRead extends EventQUICStream< + | ErrorQUICStreamLocalRead + | ErrorQUICStreamPeerRead + | undefined +> {} + +/** + * QUIC stream writable side is closed. + * + * `ErrorQUICStreamLocalWrite` - writable side aborted locally with code. + * `ErrorQUICStreamPeerWrite` - writable side aborted by peer cancelling the + * remote readable side. + * `undefined` - writable side closed gracefully. + */ +class EventQUICStreamCloseWrite extends EventQUICStream< + | ErrorQUICStreamLocalWrite + | ErrorQUICStreamPeerWrite + | undefined +> {} + +class EventQUICStreamSend extends EventQUICStream {} export { - QUICSocketEvent, - QUICSocketStartEvent, - QUICSocketStopEvent, - QUICSocketErrorEvent, - QUICClientEvent, - QUICClientDestroyEvent, - QUICClientErrorEvent, - QUICServerEvent, - QUICServerConnectionEvent, - QUICServerStartEvent, - QUICServerStopEvent, - QUICServerErrorEvent, - QUICConnectionEvent, - QUICConnectionStreamEvent, - QUICConnectionStopEvent, - QUICConnectionErrorEvent, - QUICStreamEvent, - QUICStreamDestroyEvent, + EventQUIC, + EventQUICSocket, + EventQUICSocketStart, + EventQUICSocketStarted, + EventQUICSocketStop, + EventQUICSocketStopped, + EventQUICSocketError, + EventQUICSocketClose, + EventQUICClient, + EventQUICClientDestroy, + EventQUICClientDestroyed, + EventQUICClientError, + EventQUICClientClose, + EventQUICServer, + EventQUICServerStart, + EventQUICServerStarted, + EventQUICServerStop, + EventQUICServerStopped, + EventQUICServerError, + EventQUICServerClose, + EventQUICServerConnection, + EventQUICConnection, + EventQUICConnectionStart, + EventQUICConnectionStarted, + EventQUICConnectionStop, + EventQUICConnectionStopped, + EventQUICConnectionError, + EventQUICConnectionClose, + EventQUICConnectionStream, + EventQUICConnectionSend, + EventQUICStream, + EventQUICStreamDestroy, + EventQUICStreamDestroyed, + EventQUICStreamError, + EventQUICStreamCloseRead, + EventQUICStreamCloseWrite, + EventQUICStreamSend, }; diff --git a/src/native/napi/config.rs b/src/native/napi/config.rs index e167fab2..96b6df6b 100644 --- a/src/native/napi/config.rs +++ b/src/native/napi/config.rs @@ -136,10 +136,9 @@ impl Config { |e| Err(napi::Error::from_reason(e.to_string())) )?; } - let ssl_ctx= ssl_ctx_builder.build(); - let config = quiche::Config::with_boring_ssl_ctx( + let config = quiche::Config::with_boring_ssl_ctx_builder( quiche::PROTOCOL_VERSION, - ssl_ctx, + ssl_ctx_builder, ).or_else( |e| Err(Error::from_reason(e.to_string())) )?; @@ -341,6 +340,11 @@ impl Config { ); } + #[napi] + pub fn set_max_stream_window(&mut self, v: i64) { + return self.0.set_max_stream_window(v as u64); + } + #[napi] pub fn set_max_connection_window(&mut self, v: i64) -> () { return self.0.set_max_connection_window(v as u64); diff --git a/src/native/napi/connection.rs b/src/native/napi/connection.rs index 26e3a1c2..04a2518c 100644 --- a/src/native/napi/connection.rs +++ b/src/native/napi/connection.rs @@ -2,7 +2,8 @@ use std::io; use std::fs::File; use std::net::{ SocketAddr, - ToSocketAddrs, + Ipv4Addr, + Ipv6Addr, }; use napi_derive::napi; use napi::bindgen_prelude::*; @@ -139,12 +140,18 @@ pub struct HostPort { impl TryFrom for SocketAddr { type Error = io::Error; fn try_from(host: HostPort) -> io::Result { - (host.host, host.port).to_socket_addrs()?.next().ok_or( - io::Error::new( - io::ErrorKind::Other, - "Could not convert host to socket address" - ) - ) + if let Ok(ipv4) = host.host.parse::() { + return Ok(SocketAddr::new(ipv4.into(), host.port)); + } + + if let Ok(ipv6) = host.host.parse::() { + return Ok(SocketAddr::new(ipv6.into(), host.port)); + } + + Err(io::Error::new( + io::ErrorKind::Other, + "Could not convert host to socket address", + )) } } @@ -192,34 +199,13 @@ impl Connection { remote_host: HostPort, config: &mut config::Config, ) -> napi::Result { - // These addresses are passed in from the outside - // We expect that the local address has already been bound to - // On the UDP socket, we don't do any binding here - // Since the nodejs runtime will do the relevant binding - // When binding, it needs to bind to both IPv6 and IPv6 - let local_addr: SocketAddr = local_host.try_into().or_else( |err: io::Error| Err(napi::Error::from_reason(err.to_string())) )?; - - // let local_addr = (local_host, local_port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next().unwrap(); - - // eprintln!("Local address: {:?}", local_addr); - - // let remote_addr = (remote_host, remote_port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next().unwrap(); - let remote_addr: SocketAddr = remote_host.try_into().or_else( |err: io::Error| Err(napi::Error::from_reason(err.to_string())) )?; - - // eprintln!("Remote address: {:?}", remote_addr); - let scid = quiche::ConnectionId::from_ref(&scid); - let connection = quiche::connect( server_name.as_deref(), &scid, @@ -229,9 +215,6 @@ impl Connection { ).or_else( |err| Err(napi::Error::from_reason(err.to_string())) )?; - - // eprintln!("STDERR New connection with scid {:?}", scid); - return Ok(Connection(connection)); } @@ -243,33 +226,16 @@ impl Connection { remote_host: HostPort, config: &mut config::Config, ) -> napi::Result { - - // let local_addr = (local_host, local_port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next().unwrap(); - let local_addr: SocketAddr = local_host.try_into().or_else( |err: io::Error| Err(napi::Error::from_reason(err.to_string())) )?; - - // eprintln!("Local address: {:?}", local_addr); - - // let remote_addr = (remote_host, remote_port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next().unwrap(); - let remote_addr: SocketAddr = remote_host.try_into().or_else( |err: io::Error| Err(napi::Error::from_reason(err.to_string())) )?; - - // eprintln!("Remote address: {:?}", remote_addr); - let scid = quiche::ConnectionId::from_ref(&scid); - let odcid = odcid.map( |dcid| quiche::ConnectionId::from_vec(dcid.to_vec()) ); - let connection = quiche::accept( &scid, odcid.as_ref(), @@ -279,9 +245,6 @@ impl Connection { ).or_else( |err| Err(napi::Error::from_reason(err.to_string())) )?; - - // eprintln!("New connection with scid {:?}", scid); - return Ok(Connection(connection)); } @@ -301,46 +264,20 @@ impl Connection { ); } - // This data buffer must be the size of the entire largest packet... - // It is not the max datagram size, you have to potentially take - // A VERY large packet - // On the other hand, it's all dynamic in JS - // So it may not be a problem #[napi] pub fn recv( &mut self, mut data: Uint8Array, recv_info: RecvInfo, ) -> napi::Result { - - // Parsing is kind of slow - // the from address has to be passed in from JS side - // but the local address here is already known here - // if we can keep track of it, it would work nicely - // In fact, for any given connection, don't we already have both the remote address and the local address already? - // Yea, exactly this information is technically already known - - // recv_info.from - let recv_info = quiche::RecvInfo { from: recv_info.from.try_into().or_else( |err: io::Error| Err(napi::Error::from_reason(err.to_string())) )?, - // from: (recv_info.from.addr, recv_info.from.port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next().unwrap(), to: recv_info.to.try_into().or_else( |err: io::Error| Err(napi::Error::from_reason(err.to_string())) )?, - // to: (recv_info.to.addr, recv_info.to.port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next().unwrap(), }; - // If there is an error, the JS side should continue to read - // But it can log out the error - // You may call this multiple times - // When receiving multiple packets - // Process potentially coalesced packets. let read = match self.0.recv( &mut data, recv_info @@ -358,16 +295,13 @@ impl Connection { /// The buffer must be allocated to the size of MAX_DATAGRAM_SIZE. /// This will return a JS array of `[length, send_info]`. /// It is possible for the length to be 0. - /// You may then send a 0-lenght buffer. + /// You may then send a 0-length buffer. /// If there is nothing to be sent a Done error will be thrown. #[napi(ts_return_type = "[number, SendInfo]")] - pub fn send(&mut self, env: Env, mut data: Uint8Array) -> napi::Result { - // Convert the Done error into a 0-length write - // This would mean that there's nothing to send - + pub fn send(&mut self, env: Env, mut data: Uint8Array) -> napi::Result> { let (write, send_info) = match self.0.send(&mut data) { Ok((write, send_info)) => (write, send_info), - // Err(quiche::Error::Done) => (0, None), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; let send_info = { @@ -385,12 +319,9 @@ impl Connection { let mut write_and_send_info = env.create_array(2)?; write_and_send_info.set(0, write as i64)?; write_and_send_info.set(1, send_info)?; - return Ok(write_and_send_info); + return Ok(Some(write_and_send_info)); } - // So you can pass the SocketAddr - // But instead we provide a sort of conversion that is necessary - #[napi(ts_return_type = "[number, SendInfo | null]")] pub fn send_on_path( &mut self, @@ -398,15 +329,7 @@ impl Connection { mut data: Uint8Array, from: Option, to: Option - ) -> napi::Result { - // If we want to "preserve" the error - // We have to then provide a Some(Result) - // Which means Option> - // Then we have to "unwrap" it - // But I'm not sure how to do this here... - // Especially it seems so functional - // On the other hand... I think if we can unwrap it here - + ) -> napi::Result> { let from: Option = match from { Some(host) => Some( host.try_into().or_else( @@ -415,9 +338,6 @@ impl Connection { ) )? ), - // Some(host) => (host.addr, host.port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next(), _ => None }; let to: Option = match to { @@ -425,36 +345,32 @@ impl Connection { host.try_into().or_else( |err: io::Error| Err( napi::Error::new(napi::Status::InvalidArg, err.to_string()) - // napi::Error::from_reason(err.to_string()) ) )? ), - // Some(host) => (host.addr, host.port).to_socket_addrs().or_else( - // |err| Err(napi::Error::from_reason(err.to_string())) - // )?.next(), _ => None }; let (write, send_info) = match self.0.send_on_path(&mut data, from, to) { - Ok((write, send_info)) => (write, Some(send_info)), - // Err(quiche::Error::Done) => (0, None), + Ok((write, send_info)) => (write, send_info), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; - let send_info = send_info.map(|info| { + let send_info = { let from = HostPort { - host: info.from.ip().to_string(), - port: info.from.port(), + host: send_info.from.ip().to_string(), + port: send_info.from.port(), }; let to = HostPort { - host: info.to.ip().to_string(), - port: info.to.port(), + host: send_info.to.ip().to_string(), + port: send_info.to.port(), }; - let at = External::new(info.at); + let at = External::new(send_info.at); SendInfo { from, to, at } - }); + }; let mut write_and_send_info = env.create_array(2)?; write_and_send_info.set(0, write as i64)?; write_and_send_info.set(1, send_info)?; - return Ok(write_and_send_info); + return Ok(Some(write_and_send_info)); } #[napi] @@ -483,25 +399,19 @@ impl Connection { env: Env, stream_id: i64, mut data: Uint8Array, - ) -> napi::Result { + ) -> napi::Result> { let (read, fin) = match self.0.stream_recv( stream_id as u64, &mut data, ) { Ok((read, fin)) => (read, fin), - // Change this to an exception - // DONE means it's actually done! - // Done means there's no more data to receive - // Err(quiche::Error::Done) => (0, true), - // Which is different from receiving a 0-length buffer - // We can also change this to a different kind of thing? - // But if it is a result array or something else + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; let mut read_and_fin = env.create_array(2)?; read_and_fin.set(0, read as i64)?; read_and_fin.set(1, fin)?; - return Ok(read_and_fin); + return Ok(Some(read_and_fin)); } #[napi] @@ -510,21 +420,14 @@ impl Connection { stream_id: i64, data: Uint8Array, fin: bool - ) -> napi::Result { - // 0-length buffer can be written with a fin being true - // this indicates that it has finished the stream - - // number of written bytes may be lower than the length - // of hte input buffer when the stream doesn't have enough capacity - // the app should retry the operation once the stream reports it is writable again + ) -> napi::Result> { match self.0.stream_send( stream_id as u64, &data, fin ) { - Ok(v) => return Ok(v as i64), - // We are going to just return Done - // Err(quiche::Error::Done) => return Ok(0), + Ok(v) => return Ok(Some(v as i64)), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; } @@ -549,23 +452,16 @@ impl Connection { stream_id: i64, direction: Shutdown, err: i64 - ) -> napi::Result<()> { - // The err is an application-supplied error code - // It's an application protocol error code - // https://datatracker.ietf.org/doc/html/rfc9000#section-20.2 - // I think HTTP3 uses this a bit - // RESET_STREAM means we stop sending - // It can indicate to the peer WHY we have stopped sending - // STOP_SENDING means we stop receiving - // It can indicate to the peer WHY we have stopped receiving - // But this is at the transport layer remember - return self.0.stream_shutdown( + ) -> napi::Result> { + return match self.0.stream_shutdown( stream_id as u64, direction.into(), err as u64 - ).or_else( - |err| Err(napi::Error::from_reason(err.to_string())) - ); + ) { + Ok(()) => Ok(Some(())), + Err(quiche::Error::Done) => Ok(None), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + }; } #[napi] @@ -638,12 +534,12 @@ impl Connection { pub fn dgram_recv( &mut self, mut data: Uint8Array - ) -> napi::Result { + ) -> napi::Result> { match self.0.dgram_recv( &mut data, ) { - Ok(v) => return Ok(v as i64), - // Err(quiche::Error::Done) => return Ok(0), + Ok(v) => return Ok(Some(v as i64)), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; } @@ -654,19 +550,19 @@ impl Connection { ) -> napi::Result> { match self.0.dgram_recv_vec() { Ok(v) => return Ok(Some(v.into())), - // Err(quiche::Error::Done) => return Ok(None), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; } #[napi] - pub fn dgram_recv_peek(&self, mut data: Uint8Array, len: i64) -> napi::Result { + pub fn dgram_recv_peek(&self, mut data: Uint8Array, len: i64) -> napi::Result> { match self.0.dgram_recv_peek( &mut data, len as usize, ) { - Ok(v) => return Ok(v as i64), - // Err(quiche::Error::Done) => return Ok(0), + Ok(v) => return Ok(Some(v as i64)), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())) }; } @@ -710,13 +606,12 @@ impl Connection { pub fn dgram_send( &mut self, data: Uint8Array, - ) -> napi::Result<()> { + ) -> napi::Result> { match self.0.dgram_send( &data, ) { - Ok(v) => return Ok(v), - // If no data is sent, also return Ok - // Err(quiche::Error::Done) => return Ok(()), + Ok(v) => return Ok(Some(v)), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; } @@ -725,23 +620,16 @@ impl Connection { pub fn dgram_send_vec( &mut self, data: Uint8Array - ) -> napi::Result<()> { + ) -> napi::Result> { match self.0.dgram_send_vec( data.to_vec() ) { - Ok(v) => return Ok(v), - // Err(quiche::Error::Done) => return Ok(()), + Ok(v) => return Ok(Some(v)), + Err(quiche::Error::Done) => return Ok(None), Err(e) => return Err(napi::Error::from_reason(e.to_string())), }; } - // We have Task, AsyncTask and async fn that runs things in the tokio runtime - // It seems the async task could be used here - // but I'm unclear about how the streams and shit should be done - // It seems that this is all in-memory computation - // So we should just not bother any async unless there's REAL IO - - // If an exception occurs, we have to convert to false #[napi] pub fn dgram_purge_outgoing napi::Result>( &mut self, @@ -756,10 +644,6 @@ impl Connection { ); } - /// Maximum dgram size - /// - /// Use this to determine the size of the dgrams being send and received - /// I'm not sure if this is also necessary for send and recv? #[napi] pub fn dgram_max_writable_len(&mut self) -> Option { return self.0.dgram_max_writable_len().map(|v| v as i64); @@ -830,14 +714,6 @@ impl Connection { ); } - // So the problem with ConnectionId - // is that I could make it External - // But at the same time it turns out that these are just buffers - // And the connection ID can just be maintained on the JS side - // So I can just reference those buffers - // One way is to provide a constructor - // That allows you pass a buffer in to construct it - // rather than just taking it #[napi] pub fn new_source_cid( &mut self, @@ -859,11 +735,6 @@ impl Connection { return self.0.active_source_cids() as i64; } - #[napi] - pub fn max_active_source_cids(&self) -> i64 { - return self.0.max_active_source_cids() as i64; - } - #[napi] pub fn source_cids_left(&self) -> i64 { return self.0.source_cids_left() as i64; @@ -876,7 +747,6 @@ impl Connection { ); } - // Technically this is some sort of struct #[napi(ts_return_type = "object")] pub fn path_event_next( &mut self, @@ -891,11 +761,6 @@ impl Connection { #[napi] pub fn retired_scid_next(&mut self) -> Option { return self.0.retired_scid_next().map(|v| v.into()); - // return self.0.retired_scid_next().map(|v| ConnectionId(v)); - // let connection_id = self.0.retired_scid_next(); - // return connection_id.map(|v| ConnectionId { - // id: v.as_ref().into() - // }); } #[napi] @@ -915,10 +780,12 @@ impl Connection { } #[napi] - pub fn close(&mut self, app: bool, err: i64, reason: Uint8Array) -> napi::Result<()> { - return self.0.close(app, err as u64, &reason).or_else( - |e| Err(napi::Error::from_reason(e.to_string())) - ); + pub fn close(&mut self, app: bool, err: i64, reason: Uint8Array) -> napi::Result> { + return match self.0.close(app, err as u64, &reason) { + Ok(_) => Ok(Some(())), + Err(quiche::Error::Done) => Ok(None), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + }; } #[napi] @@ -950,36 +817,14 @@ impl Connection { return self.0.session().map(|s| s.to_vec().into()); } - // This requires working on a Buffer/Uint8Array - // We return the ConnectionId - // But the problem is that on the JS side - // ConnectionId is just an opaque object - // It should be "containing" an inherent buffer - // Or we just use Uint8Array as our ConnectionId - // And just do the conversion directly - // As on the JS side it makes more sense to just say that it is a buffer - // without further work - // We could do something like - // ConnectionId(Uint8Array) - // Thus wrapping it into something we can use outside - // and exposing it too? - #[napi] pub fn source_id(&self) -> Uint8Array { return self.0.source_id().as_ref().into(); - // return ConnectionId { id: self.0.source_id().as_ref().into() }; - // return ConnectionId( - // quiche::ConnectionId::from_vec(self.0.source_id().as_ref().to_vec()) - // ); } #[napi] pub fn destination_id(&self) -> Uint8Array { return self.0.destination_id().as_ref().into(); - // return ConnectionId { id: self.0.destination_id().as_ref().into() }; - // return ConnectionId( - // quiche::ConnectionId::from_vec(self.0.destination_id().as_ref().to_vec()) - // ); } #[napi] @@ -1030,9 +875,7 @@ impl Connection { #[napi] pub fn is_closed(&self) -> bool { - // eprintln!("RUST: CALLING IS_CLOSED"); let x = self.0.is_closed(); - // eprintln!("RUST: FINISH CALLING IS_CLOSED======="); return x; } diff --git a/src/native/napi/constants.rs b/src/native/napi/constants.rs index 8007bd50..86b18982 100644 --- a/src/native/napi/constants.rs +++ b/src/native/napi/constants.rs @@ -22,9 +22,20 @@ pub const MAX_DATAGRAM_SIZE: i64 = 1350; #[napi] pub const MAX_UDP_PACKET_SIZE: i64 = 65535; -// We don't need this anymore... -// pub const HTTP_3: [&[u8]; 4] = [b"h3", b"h3-29", b"h3-28", b"h3-27"]; -// let alpns: Vec<&'static [u8]> = HTTP_3.to_vec(); -// config.set_application_protos(&alpns).or_else( -// |err| Err(Error::from_reason(err.to_string())) -// )?; +/// The maximum size of the receiver connection flow control window. +/// Note that this is not exported by quiche, but it is 24 MiB +/// This is the default fro `set_max_connection_window` +#[napi] +pub const MAX_CONNECTION_WINDOW: i64 = 24 * 1024 * 1024; + +/// The maximum size of the receiver stream flow control window. +/// This is the default for `set_max_stream_window` +/// This is not exported by quiche, but it's 16 MiB +#[napi] +pub const MAX_STREAM_WINDOW: i64 = 16 * 1024 * 1024; + +#[napi] +pub const CRYPTO_ERROR_START: u16 = 0x0100; + +#[napi] +pub const CRYPTO_ERROR_STOP: u16 = 0x01FF; diff --git a/src/native/napi/path.rs b/src/native/napi/path.rs index fe9b7732..3b959729 100644 --- a/src/native/napi/path.rs +++ b/src/native/napi/path.rs @@ -1,8 +1,5 @@ use napi_derive::napi; use napi::bindgen_prelude::*; -// use napi::bindgen_prelude::{ -// Generator -// }; use serde::{Serialize, Deserialize}; use crate::connection; @@ -74,9 +71,6 @@ impl Generator for HostIter { } /// Equivalent to quiche::PathStats -/// -/// This is missing the validation_state because it is in a private module -/// that I cannot access #[napi(object)] pub struct PathStats { pub local_host: connection::HostPort, diff --git a/src/native/quiche.ts b/src/native/quiche.ts index 369bf9f5..0a3dfc44 100644 --- a/src/native/quiche.ts +++ b/src/native/quiche.ts @@ -21,6 +21,10 @@ interface Quiche { PROTOCOL_VERSION: number; MAX_DATAGRAM_SIZE: number; MAX_UDP_PACKET_SIZE: number; + MAX_STREAM_WINDOW: number; + MAX_CONNECTION_WINDOW: number; + CRYPTO_ERROR_START: number; + CRYPTO_ERROR_STOP: number; CongestionControlAlgorithm: typeof CongestionControlAlgorithm; Shutdown: typeof Shutdown; Type: typeof Type; diff --git a/src/native/types.ts b/src/native/types.ts index 93b2ba28..80786b2b 100644 --- a/src/native/types.ts +++ b/src/native/types.ts @@ -36,6 +36,7 @@ interface Config { recvQueueLen: number, sendQueueLen: number, ): void; + setMaxStreamWindow(v: number): void; setMaxConnectionWindow(v: number): void; setStatelessResetToken(v?: bigint | undefined | null): void; setDisableDcidReuse(v: boolean): void; @@ -57,18 +58,22 @@ interface Connection { setKeylog(path: string): void; setSession(session: Uint8Array): void; recv(data: Uint8Array, recvInfo: RecvInfo): number; - send(data: Uint8Array): [number, SendInfo]; + send(data: Uint8Array): [number, SendInfo] | null; sendOnPath( data: Uint8Array, from?: HostPort | undefined | null, to?: HostPort | undefined | null, - ): [number, SendInfo | null]; + ): [number, SendInfo] | null; sendQuantum(): number; sendQuantumOnPath(localHost: HostPort, peerHost: HostPort): number; - streamRecv(streamId: number, data: Uint8Array): [number, boolean]; - streamSend(streamId: number, data: Uint8Array, fin: boolean): number; + streamRecv(streamId: number, data: Uint8Array): [number, boolean] | null; + streamSend(streamId: number, data: Uint8Array, fin: boolean): number | null; streamPriority(streamId: number, urgency: number, incremental: boolean): void; - streamShutdown(streamId: number, direction: Shutdown, err: number): void; + streamShutdown( + streamId: number, + direction: Shutdown, + err: number, + ): void | null; streamCapacity(streamId: number): number; streamReadable(streamId: number): boolean; streamWritable(streamId: number, len: number): boolean; @@ -78,9 +83,9 @@ interface Connection { readable(): StreamIter; writable(): StreamIter; maxSendUdpPayloadSize(): number; - dgramRecv(data: Uint8Array): number; + dgramRecv(data: Uint8Array): number | null; dgramRecvVec(): Uint8Array | null; - dgramRecvPeek(data: Uint8Array, len: number): number; + dgramRecvPeek(data: Uint8Array, len: number): number | null; dgramRecvFrontLen(): number | null; dgramRecvQueueLen(): number; dgramRecvQueueByteSize(): number; @@ -88,8 +93,8 @@ interface Connection { dgramSendQueueByteSize(): number; isDgramSendQueueFull(): boolean; isDgramRecvQueueFull(): boolean; - dgramSend(data: Uint8Array): void; - dgramSendVec(data: Uint8Array): void; + dgramSend(data: Uint8Array): void | null; + dgramSendVec(data: Uint8Array): void | null; dgramPurgeOutgoing(f: (arg0: Uint8Array) => boolean): void; dgramMaxWritableLen(): number | null; timeout(): number | null; @@ -110,7 +115,7 @@ interface Connection { retiredScidNext(): Uint8Array | null; availableDcids(): number; pathsIter(from: HostPort): HostIter; - close(app: boolean, err: number, reason: Uint8Array): void; + close(app: boolean, err: number, reason: Uint8Array): void | null; traceId(): string; applicationProto(): Uint8Array; serverName(): string | null; @@ -183,6 +188,13 @@ enum Type { Short = 5, } +/** + * QUIC transport error codes + * https://www.rfc-editor.org/rfc/rfc9000#section-20.1 + * Note that `CryptoError` is a range of error codes. + * Therefore it is not featured in this enum. + * You can instead fetch it from the constants. + */ enum ConnectionErrorCode { NoError = 0, InternalError = 1, @@ -203,6 +215,43 @@ enum ConnectionErrorCode { NoViablePath = 16, } +/** + * CryptoError is a range from `0x100` to `0x01FF`. + * It maps from the TLS `AlertDescription` codes, offset + * by `0x100`. These are known codes of TLS 1.3 hardcoded in + * QUIC RFC 9000. + * See the TLS 1.3 codes in: https://www.rfc-editor.org/rfc/rfc8446#section-6 + */ +enum CryptoError { + CloseNotify = 256, + UnexpectedMessage = 266, + BadRecordMac = 276, + RecordOverflow = 278, + HandshakeFailure = 296, + BadCertificate = 298, + UnsupportedCertificate = 299, + CertificateRevoked = 300, + CertificateExpired = 301, + CertificateUnknown = 302, + IllegalParameter = 303, + UnknownCA = 304, + AccessDenied = 305, + DecodeError = 306, + DecryptError = 307, + ProtocolVersion = 326, + InsufficientSecurity = 327, + InternalError = 336, + InappropriateFallback = 342, + UserCanceled = 346, + MissingExtension = 365, + UnsupportedExtension = 366, + UnrecognizedName = 368, + BadCertificateStatusResponse = 369, + UnknownPSKIdentity = 371, + CertificateRequired = 372, + NoApplicationProtocol = 376, +} + type ConnectionError = { isApp: boolean; errorCode: number; @@ -313,7 +362,13 @@ type PathStatsIter = { [Symbol.iterator](): Iterator; }; -export { CongestionControlAlgorithm, Shutdown, Type, ConnectionErrorCode }; +export { + CongestionControlAlgorithm, + Shutdown, + Type, + ConnectionErrorCode, + CryptoError, +}; export type { QuicheTimeInstant, diff --git a/src/types.ts b/src/types.ts index 5be92007..851b37a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,5 @@ import type QUICStream from './QUICStream'; +import type { CryptoError } from './native'; /** * Opaque types are wrappers of existing types @@ -7,6 +8,8 @@ import type QUICStream from './QUICStream'; type Opaque = T & { readonly [brand]: K }; declare const brand: unique symbol; +type Class = new (...args: any[]) => T; + /** * Generic callback */ @@ -24,33 +27,6 @@ type PromiseDeconstructed = { rejectP: (reason?: any) => void; }; -type ConnectionId = Opaque<'ConnectionId', Buffer>; - -type ConnectionIdString = Opaque<'ConnectionIdString', string>; - -/** - * Client crypto utility object - * Remember every Node Buffer is an ArrayBuffer - */ -type ClientCrypto = { - randomBytes(data: ArrayBuffer): Promise; -}; - -/** - * Server crypto utility object - * Remember every Node Buffer is an ArrayBuffer - */ -type ServerCrypto = { - sign(key: ArrayBuffer, data: ArrayBuffer): Promise; - verify( - key: ArrayBuffer, - data: ArrayBuffer, - sig: ArrayBuffer, - ): Promise; -}; - -type StreamId = Opaque<'StreamId', number>; - /** * Host is always an IP address */ @@ -71,38 +47,59 @@ type Port = Opaque<'Port', number>; */ type Address = Opaque<'Address', string>; -type QUICStreamMap = Map; - type RemoteInfo = { host: Host; port: Port; }; /** - * Maps reason (most likely an exception) to a stream code. - * Use `0` to indicate unknown/default reason. + * Client crypto utility object + * Remember every Node Buffer is an ArrayBuffer */ -type StreamReasonToCode = ( - type: 'recv' | 'send', - reason?: any, -) => number | PromiseLike; +type ClientCryptoOps = { + randomBytes(data: ArrayBuffer): Promise; +}; /** - * Maps code to a reason. 0 usually indicates unknown/default reason. + * Server crypto utility object + * Remember every Node Buffer is an ArrayBuffer */ -type StreamCodeToReason = ( - type: 'recv' | 'send', - code: number, -) => any | PromiseLike; +type ServerCryptoOps = { + sign(key: ArrayBuffer, data: ArrayBuffer): Promise; + verify( + key: ArrayBuffer, + data: ArrayBuffer, + sig: ArrayBuffer, + ): Promise; +}; -type ConnectionMetadata = { - remoteCertificates: Array | null; - localHost: string; - localPort: number; - remoteHost: string; - remotePort: number; +type QUICClientCrypto = { + ops: ClientCryptoOps; +}; + +type QUICServerCrypto = { + key: ArrayBuffer; + ops: ServerCryptoOps; }; +/** + * Custom hostname resolution. It is expected this returns an IP address. + */ +type ResolveHostname = (hostname: string) => string | PromiseLike; + +/** + * Custom TLS verification callback. + * The peer cert chain will be passed as the first parameter. + * The CA certs will also be available as a second parameter. + * The certs are in DER binary format. + * It will be an empty array if there were no CA certs. + * If it fails, return a `CryptoError` code. + */ +type TLSVerifyCallback = ( + certs: Array, + ca: Array, +) => PromiseLike; + type QUICConfig = { /** * Certificate authority certificate in PEM format or Uint8Array buffer @@ -165,10 +162,12 @@ type QUICConfig = { verifyPeer: boolean; /** - * Will allow insecure TLS certs, allowing for certs to be requested - * but the verification result is ignored. + * Custom TLS verification callback. + * It is expected that the callback will throw an error if the verification + * fails. + * Will be ignored if `verifyPeer` is false. */ - verifyAllowFail: boolean; + verifyCallback?: TLSVerifyCallback; /** * Enables the logging of secret keys to a file path. @@ -274,6 +273,16 @@ type QUICConfig = { */ initialMaxStreamsUni: number; + /** + * This defaults to 24 MiB. + */ + maxConnectionWindow: number; + + /** + * This defaults to 16 MiB. + */ + maxStreamWindow: number; + /** * Enables receiving dgram. * The 2 numbers are receive queue length and send queue length. @@ -298,26 +307,66 @@ type QUICConfig = { enableEarlyData: boolean; }; -type VerifyCallback = (certs: Array) => Promise | void; +type QUICClientConfigInput = Partial; + +type QUICServerConfigInput = Partial & { + key: string | Array | Uint8Array | Array; + cert: string | Array | Uint8Array | Array; +}; + +type ConnectionId = Opaque<'ConnectionId', Buffer>; + +type ConnectionIdString = Opaque<'ConnectionIdString', string>; + +type ConnectionMetadata = { + localHost: string; + localPort: number; + remoteHost: string; + remotePort: number; + localCertsChain: Array; + localCACertsChain: Array; + remoteCertsChain: Array; +}; + +type StreamId = Opaque<'StreamId', number>; + +/** + * Maps reason (most likely an exception) to a stream code. + * Use `0` to indicate unknown/default reason. + */ +type StreamReasonToCode = (type: 'read' | 'write', reason?: any) => number; + +/** + * Maps code to a reason. 0 usually indicates unknown/default reason. + */ +type StreamCodeToReason = (type: 'read' | 'write', code: number) => any; + +type QUICStreamMap = Map; export type { Opaque, + Class, Callback, PromiseDeconstructed, - ConnectionId, - ConnectionIdString, - ClientCrypto, - ServerCrypto, - StreamId, Host, Hostname, Port, Address, - QUICStreamMap, RemoteInfo, + ClientCryptoOps, + ServerCryptoOps, + QUICClientCrypto, + QUICServerCrypto, + ResolveHostname, + TLSVerifyCallback, + QUICConfig, + QUICClientConfigInput, + QUICServerConfigInput, + ConnectionId, + ConnectionIdString, + ConnectionMetadata, + StreamId, StreamReasonToCode, StreamCodeToReason, - ConnectionMetadata, - QUICConfig, - VerifyCallback, + QUICStreamMap, }; diff --git a/src/utils.ts b/src/utils.ts index 179acadf..6568d953 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,18 +1,94 @@ import type { + Class, Callback, PromiseDeconstructed, - ConnectionId, - ConnectionIdString, Host, Port, - ServerCrypto, + QUICServerCrypto, + ConnectionId, + ConnectionIdString, + StreamId, } from './types'; -import type { Connection } from './native'; import dns from 'dns'; import { IPv4, IPv6, Validator } from 'ip-num'; import QUICConnectionId from './QUICConnectionId'; import * as errors from './errors'; +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder('utf-8'); + +/** + * Convert callback-style to promise-style + * If this is applied to overloaded function + * it will only choose one of the function signatures to use + */ +function promisify< + T extends Array, + P extends Array, + R extends T extends [] ? void : T extends [unknown] ? T[0] : T, +>( + f: (...args: [...params: P, callback: Callback]) => unknown, +): (...params: P) => Promise { + // Uses a regular function so that `this` can be bound + return function (...params: P): Promise { + return new Promise((resolve, reject) => { + const callback = (error, ...values) => { + if (error != null) { + return reject(error); + } + if (values.length === 0) { + (resolve as () => void)(); + } else if (values.length === 1) { + resolve(values[0] as R); + } else { + resolve(values as R); + } + return; + }; + params.push(callback); + f.apply(this, params); + }); + }; +} + +/** + * Deconstructed promise + */ +function promise(): PromiseDeconstructed { + let resolveP, rejectP; + const p = new Promise((resolve, reject) => { + resolveP = resolve; + rejectP = reject; + }); + return { + p, + resolveP, + rejectP, + }; +} + +/** + * Zero-copy wraps ArrayBuffer-like objects into Buffer + * This supports ArrayBuffer, TypedArrays and the NodeJS Buffer + */ +function bufferWrap( + array: BufferSource, + offset?: number, + length?: number, +): Buffer { + if (Buffer.isBuffer(array)) { + return array; + } else if (ArrayBuffer.isView(array)) { + return Buffer.from( + array.buffer, + offset ?? array.byteOffset, + length ?? array.byteLength, + ); + } else { + return Buffer.from(array, offset, length); + } +} + /** * Is it an IPv4 address? */ @@ -125,11 +201,21 @@ function fromIPv4MappedIPv6(host: string): Host { return ipv4Decs.join('.') as Host; } +function isHostWildcard(host: Host): boolean { + return ( + host === '0.0.0.0' || + host === '::' || + host === '::0' || + host === '::ffff:0.0.0.0' || + host === '::ffff:0:0' + ); +} + /** * This converts all `IPv4` formats to the `IPv4` decimal format. * `IPv4` decimal and `IPv6` hex formatted IPs are left unchanged. */ -function toCanonicalIp(host: string) { +function toCanonicalIP(host: string) { if (isIPv4MappedIPv6(host)) { return fromIPv4MappedIPv6(host); } @@ -139,6 +225,29 @@ function toCanonicalIp(host: string) { throw new TypeError('Invalid IP address'); } +/** + * Zero IPs should be resolved to localhost when used as the target + */ +function resolvesZeroIP(host: Host): Host { + const zeroIPv4 = new IPv4('0.0.0.0'); + // This also covers `::0` + const zeroIPv6 = new IPv6('::'); + if (isIPv4MappedIPv6(host)) { + const ipv4 = fromIPv4MappedIPv6(host); + if (new IPv4(ipv4).isEquals(zeroIPv4)) { + return toIPv4MappedIPv6Dec('127.0.0.1'); + } else { + return host; + } + } else if (isIPv4(host) && new IPv4(host).isEquals(zeroIPv4)) { + return '127.0.0.1' as Host; + } else if (isIPv6(host) && new IPv6(host).isEquals(zeroIPv6)) { + return '::1' as Host; + } else { + return host; + } +} + /** * This will resolve a hostname to the first host. * It could be an IPv6 address or IPv4 address. @@ -159,7 +268,7 @@ async function resolveHostname(hostname: string): Promise { */ async function resolveHost( host: string, - resolveHostname: (hostname: string) => Host | PromiseLike, + resolveHostname: (hostname: string) => string | PromiseLike, ): Promise<[Host, 'udp4' | 'udp6']> { if (isIPv4(host)) { return [host as Host, 'udp4']; @@ -175,13 +284,6 @@ async function resolveHost( } } -/** - * Converts a Host back to a string. - */ -function fromHost(host: Host): string { - return host; -} - /** * Is it a valid Port? */ @@ -198,85 +300,6 @@ function toPort(port: any): Port { return port; } -/** - * Converts a Port back to a number. - */ -function fromPort(port: Port): number { - return port; -} - -/** - * Convert callback-style to promise-style - * If this is applied to overloaded function - * it will only choose one of the function signatures to use - */ -function promisify< - T extends Array, - P extends Array, - R extends T extends [] ? void : T extends [unknown] ? T[0] : T, ->( - f: (...args: [...params: P, callback: Callback]) => unknown, -): (...params: P) => Promise { - // Uses a regular function so that `this` can be bound - return function (...params: P): Promise { - return new Promise((resolve, reject) => { - const callback = (error, ...values) => { - if (error != null) { - return reject(error); - } - if (values.length === 0) { - (resolve as () => void)(); - } else if (values.length === 1) { - resolve(values[0] as R); - } else { - resolve(values as R); - } - return; - }; - params.push(callback); - f.apply(this, params); - }); - }; -} - -/** - * Deconstructed promise - */ -function promise(): PromiseDeconstructed { - let resolveP, rejectP; - const p = new Promise((resolve, reject) => { - resolveP = resolve; - rejectP = reject; - }); - return { - p, - resolveP, - rejectP, - }; -} - -/** - * Zero-copy wraps ArrayBuffer-like objects into Buffer - * This supports ArrayBuffer, TypedArrays and the NodeJS Buffer - */ -function bufferWrap( - array: BufferSource, - offset?: number, - length?: number, -): Buffer { - if (Buffer.isBuffer(array)) { - return array; - } else if (ArrayBuffer.isView(array)) { - return Buffer.from( - array.buffer, - offset ?? array.byteOffset, - length ?? array.byteLength, - ); - } else { - return Buffer.from(array, offset, length); - } -} - /** * Given host and port, create an address string. */ @@ -292,53 +315,111 @@ function buildAddress(host: string, port: number = 0): string { return address; } -function isHostWildcard(host: Host): boolean { - return ( - host === '0.0.0.0' || - host === '::' || - host === '::0' || - host === '::ffff:0.0.0.0' || - host === '::ffff:0:0' - ); +function validateTarget( + socketHost: Host, + socketType: 'ipv4' | 'ipv6' | 'ipv4&ipv6', + targetHost: Host, + targetUdpType: 'udp4' | 'udp6', + errorClass: Class, +): Host { + if (isHostWildcard(targetHost)) { + throw new errorClass(`Invalid wildcard target host ${targetHost}`); + } + const isSocketHostIPv4Mapped = isIPv4MappedIPv6(socketHost); + const isTargetHostIPv4Mapped = isIPv4MappedIPv6(targetHost); + if (socketType === 'ipv4&ipv6' && targetUdpType === 'udp4') { + // If socket is IPv4 and IPv6 then: + // If target is IPv4 - wrap and pass + // If target is IPv6 - pass + // If target is IPv4 mapped IPv6 - pass + return toIPv4MappedIPv6Dec(targetHost); + } + if (socketType === 'ipv4') { + if (!isSocketHostIPv4Mapped) { + // If socket is IPv4 then: + // if target is IPv4 - pass + // if target is IPv6 - fail + // if target is IPv4 mapped IPv6 - unwrap and pass + if (targetUdpType === 'udp6') { + if (isTargetHostIPv4Mapped) { + return fromIPv4MappedIPv6(targetHost); + } else { + throw new errorClass( + `Invalid target host ${targetHost} from an IPv4 socket`, + ); + } + } + } else { + // If socket is IPv4 but uses IPv4 mapped IPv6 bound address then: + // If target is IPv4 - wrap and pass + // If target is IPv6 - fail + // If target is IPv4 mapped IPv6 - pass + if (targetUdpType === 'udp4') { + return toIPv4MappedIPv6Dec(targetHost); + } else if (targetUdpType === 'udp6' && !isTargetHostIPv4Mapped) { + throw new errorClass( + `Invalid target host ${targetHost} from an IPv4 socket`, + ); + } + } + return targetHost; + } + if (socketType === 'ipv6') { + // If socket is IPv6 then: + // If target is IPv4 - fail + // If target is IPv6 - pass + // If target is IPv4 mapped IPv6 - fail + if (targetUdpType === 'udp4' || isTargetHostIPv4Mapped) { + throw new errorClass( + `Invalid target host ${targetHost} from an IPv6 socket`, + ); + } + return targetHost; + } + return targetHost; } /** - * Zero IPs should be resolved to localhost when used as the target + * Collects PEM arrays specified in `QUICConfig` into a PEM chain array. + * This can be used for keys, certs and ca. */ -function resolvesZeroIP(host: Host): Host { - const zeroIPv4 = new IPv4('0.0.0.0'); - // This also covers `::0` - const zeroIPv6 = new IPv6('::'); - if (isIPv4MappedIPv6(host)) { - const ipv4 = fromIPv4MappedIPv6(host); - if (new IPv4(ipv4).isEquals(zeroIPv4)) { - return toIPv4MappedIPv6Dec('127.0.0.1'); - } else { - return host; +function collectPEMs( + pems?: string | Array | Uint8Array | Array, +): Array { + const pemsChain: Array = []; + if (typeof pems === 'string') { + pemsChain.push(pems.trim() + '\n'); + } else if (pems instanceof Uint8Array) { + pemsChain.push(textDecoder.decode(pems).trim() + '\n'); + } else if (Array.isArray(pems)) { + for (const c of pems) { + if (typeof c === 'string') { + pemsChain.push(c.trim() + '\n'); + } else { + pemsChain.push(textDecoder.decode(c).trim() + '\n'); + } } - } else if (isIPv4(host) && new IPv4(host).isEquals(zeroIPv4)) { - return '127.0.0.1' as Host; - } else if (isIPv6(host) && new IPv6(host).isEquals(zeroIPv6)) { - return '::1' as Host; - } else { - return host; } + return pemsChain; } -function encodeConnectionId(connId: ConnectionId): ConnectionIdString { - return connId.toString('hex') as ConnectionIdString; -} - -function decodeConnectionId(connIdString: ConnectionIdString): ConnectionId { - return Buffer.from(connIdString, 'hex') as ConnectionId; -} - -function never(message?: string): never { - throw new errors.ErrorQUICUndefinedBehaviour(message); +/** + * Converts PEM strings to DER Uint8Array + */ +function pemToDER(pem: string): Uint8Array { + const pemB64 = pem + .replace(/-----BEGIN .*-----/, '') + .replace(/-----END .*-----/, '') + .replace(/\s+/g, ''); + const der = Buffer.from(pemB64, 'base64'); + return new Uint8Array(der); } -function certificateDERToPEM(der: Uint8Array): string { - const data = Buffer.from(der); +/** + * Converts DER Uint8Array to PEM string + */ +function derToPEM(der: Uint8Array): string { + const data = Buffer.from(der.buffer, der.byteOffset, der.byteLength); const contents = data .toString('base64') @@ -347,21 +428,28 @@ function certificateDERToPEM(der: Uint8Array): string { return `-----BEGIN CERTIFICATE-----\n${contents}-----END CERTIFICATE-----\n`; } -function certificatePEMsToCertChainPem(pems: Array): string { - let certChainPEM = ''; - for (const pem of pems) { - certChainPEM += pem; - } - return certChainPEM; +/** + * Formats error exceptions. + * Example: `Error: description - message` + */ +function formatError(error: Error): string { + return `${error.name}${ + 'description' in error ? `: ${error.description}` : '' + }${error.message !== undefined ? ` - ${error.message}` : ''}`; +} + +function encodeConnectionId(connId: ConnectionId): ConnectionIdString { + return connId.toString('hex') as ConnectionIdString; +} + +function decodeConnectionId(connIdString: ConnectionIdString): ConnectionId { + return Buffer.from(connIdString, 'hex') as ConnectionId; } async function mintToken( dcid: QUICConnectionId, peerHost: Host, - crypto: { - key: ArrayBuffer; - ops: ServerCrypto; - }, + crypto: QUICServerCrypto, ): Promise { const msgData = { dcid: dcid.toString(), host: peerHost }; const msgJSON = JSON.stringify(msgData); @@ -378,10 +466,7 @@ async function mintToken( async function validateToken( tokenBuffer: Buffer, peerHost: Host, - crypto: { - key: ArrayBuffer; - ops: ServerCrypto; - }, + crypto: QUICServerCrypto, ): Promise { let tokenData; try { @@ -418,51 +503,56 @@ async function validateToken( return QUICConnectionId.fromString(msgData.dcid); } -async function sleep(ms: number): Promise { - return await new Promise((r) => setTimeout(r, ms)); +function isStreamClientInitiated(streamId: StreamId): boolean { + return (streamId & 0b01) === 0; +} + +function isStreamServerInitiated(streamId: StreamId): boolean { + return (streamId & 0b01) === 1; +} + +function isStreamUnidirectional(streamId: StreamId): boolean { + return (streamId & 0b10) !== 0; +} + +function isStreamBidirectional(streamId: StreamId): boolean { + return (streamId & 0b10) === 0; } /** - * Useful for debug printing stream state + * Note if the peer sends a corrupted `StreamStopped`, the `code` will be `NaN` + * Furthermore it is limited to 16 digits the stringified maximum integer size of JS. */ -function streamStats( - connection: Connection, - streamId: number, - label: string, -): string { - let streamWritable: string; - try { - streamWritable = `${connection.streamWritable(streamId, 0)}`; - } catch (e) { - streamWritable = `threw ${e.message}`; - } - let streamCapacity: string; - try { - streamCapacity = `${connection.streamCapacity(streamId)}`; - } catch (e) { - streamCapacity = `threw ${e.message}`; - } - let readableIterator = false; - for (const streamIterElement of connection.readable()) { - if (streamIterElement === streamId) readableIterator = true; +function isStreamStopped(e: Error): number | false { + let match: RegExpMatchArray | null; + if ((match = e.message.match(/StreamStopped\((\d{1,16})\)/)) != null) { + const code = parseInt(match[1]); + return code; + } else { + return false; } - let writableIterator = false; - for (const streamIterElement of connection.writable()) { - if (streamIterElement === streamId) writableIterator = true; +} + +/** + * Note if the peer sends a corrupted `StreamReset`, the `code` will be `NaN` + * Furthermore it is limited to 16 digits the stringified maximum integer size of JS. + */ +function isStreamReset(e: Error): number | false { + let match: RegExpMatchArray | null; + if ((match = e.message.match(/StreamReset\((\d{1,16})\)/)) != null) { + const code = parseInt(match[1]); + return code; + } else { + return false; } - return ` - ---${label}--- - isReadable: ${connection.isReadable()}, - readable iterator: ${readableIterator}, - streamReadable: ${connection.streamReadable(streamId)}, - streamFinished: ${connection.streamFinished(streamId)}, - writable iterator: ${writableIterator}, - streamWritable: ${streamWritable}, - streamCapacity: ${streamCapacity}, -`; } export { + textEncoder, + textDecoder, + promisify, + promise, + bufferWrap, isIPv4, isIPv6, isIPv4MappedIPv6, @@ -471,26 +561,27 @@ export { toIPv4MappedIPv6Dec, toIPv4MappedIPv6Hex, fromIPv4MappedIPv6, - toCanonicalIp, + isHostWildcard, + toCanonicalIP, + resolvesZeroIP, resolveHostname, resolveHost, - fromHost, isPort, toPort, - fromPort, - promisify, - promise, - bufferWrap, buildAddress, - resolvesZeroIP, - isHostWildcard, + validateTarget, + collectPEMs, + pemToDER, + derToPEM, + formatError, encodeConnectionId, decodeConnectionId, - never, - certificateDERToPEM, - certificatePEMsToCertChainPem, mintToken, validateToken, - sleep, - streamStats, + isStreamClientInitiated, + isStreamServerInitiated, + isStreamBidirectional, + isStreamUnidirectional, + isStreamStopped, + isStreamReset, }; diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 082fc0e1..f32eb8f4 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -1,5 +1,4 @@ -import type { ClientCrypto, ServerCrypto } from '@/types'; -import type * as events from '@/events'; +import type { ClientCryptoOps, ServerCryptoOps } from '@/types'; import type QUICConnection from '@/QUICConnection'; import type { KeyTypes, TLSConfigs } from './utils'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; @@ -10,8 +9,10 @@ import QUICClient from '@/QUICClient'; import QUICServer from '@/QUICServer'; import * as errors from '@/errors'; import { promise } from '@/utils'; +import * as events from '@/events'; +import { CryptoError } from '@/native'; import * as testsUtils from './utils'; -import { generateConfig, sleep } from './utils'; +import { generateTLSConfig, sleep } from './utils'; describe(QUICClient.name, () => { const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ @@ -21,53 +22,50 @@ describe(QUICClient.name, () => { ]); const localhost = '127.0.0.1'; // This has to be setup asynchronously due to key generation - const serverCrypto: ServerCrypto = { + const serverCryptoOps: ServerCryptoOps = { sign: testsUtils.signHMAC, verify: testsUtils.verifyHMAC, }; let key: ArrayBuffer; - const clientCrypto: ClientCrypto = { + const clientCryptoOps: ClientCryptoOps = { randomBytes: testsUtils.randomBytes, }; - let sockets: Set; + let socketCleanMethods: ReturnType; - const types: Array = ['RSA', 'ECDSA', 'ED25519']; + const types: Array = ['RSA', 'ECDSA', 'Ed25519']; + // Const types: Array = ['RSA']; const defaultType = types[0]; // We need to test the stream making beforeEach(async () => { key = await testsUtils.generateKeyHMAC(); - sockets = new Set(); + socketCleanMethods = testsUtils.socketCleanupFactory(); }); afterEach(async () => { - const stopProms: Array> = []; - for (const socket of sockets) { - stopProms.push(socket.stop({ force: true })); - } - await Promise.allSettled(stopProms); + await socketCleanMethods.stopSockets(); }); // Are we describing a dual stack client!? describe('dual stack client', () => { test('to ipv4 server succeeds', async () => { - const tlsConfigServer = await testsUtils.generateConfig(defaultType); + const tlsConfigServer = await testsUtils.generateTLSConfig(defaultType); - const connectionEventProm = promise(); + const connectionEventProm = promise(); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigServer.key, - cert: tlsConfigServer.cert, + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -78,41 +76,41 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; expect(conn.localHost).toBe('127.0.0.1'); expect(conn.localPort).toBe(server.port); expect(conn.remoteHost).toBe('127.0.0.1'); - expect(conn.remotePort).toBe(client.port); + expect(conn.remotePort).toBe(client.localPort); await client.destroy(); await server.stop(); }); test('to ipv6 server succeeds', async () => { - const connectionEventProm = promise(); - const tlsConfigServer = await testsUtils.generateConfig(defaultType); + const connectionEventProm = promise(); + const tlsConfigServer = await testsUtils.generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigServer.key, - cert: tlsConfigServer.cert, + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -124,41 +122,41 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; expect(conn.localHost).toBe('::1'); expect(conn.localPort).toBe(server.port); expect(conn.remoteHost).toBe('::1'); - expect(conn.remotePort).toBe(client.port); + expect(conn.remotePort).toBe(client.localPort); await client.destroy(); await server.stop(); }); test('to dual stack server succeeds', async () => { - const connectionEventProm = promise(); - const tlsConfigServer = await testsUtils.generateConfig(defaultType); + const connectionEventProm = promise(); + const tlsConfigServer = await testsUtils.generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigServer.key, - cert: tlsConfigServer.cert, + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -170,19 +168,19 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; expect(conn.localHost).toBe('::'); expect(conn.localPort).toBe(server.port); expect(conn.remoteHost).toBe('::1'); - expect(conn.remotePort).toBe(client.port); + expect(conn.remotePort).toBe(client.localPort); await client.destroy(); await server.stop(); }); @@ -196,7 +194,7 @@ describe(QUICClient.name, () => { port: 56666, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -204,7 +202,7 @@ describe(QUICClient.name, () => { verifyPeer: false, }, }), - ).rejects.toThrow(errors.ErrorQUICConnectionStartTimeOut); + ).rejects.toThrow(errors.ErrorQUICConnectionIdleTimeout); }); test('intervalTimeoutTime must be less than maxIdleTimeout', async () => { // Larger keepAliveIntervalTime throws @@ -214,7 +212,7 @@ describe(QUICClient.name, () => { port: 56666, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -223,7 +221,7 @@ describe(QUICClient.name, () => { verifyPeer: false, }, }), - ).rejects.toThrow(errors.ErrorQUICConnectionInvalidConfig); + ).rejects.toThrow(errors.ErrorQUICConnectionConfigInvalid); // Smaller keepAliveIntervalTime doesn't cause a problem await expect( QUICClient.createQUICClient({ @@ -231,7 +229,7 @@ describe(QUICClient.name, () => { port: 56666, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -240,7 +238,7 @@ describe(QUICClient.name, () => { verifyPeer: false, }, }), - ).rejects.not.toThrow(errors.ErrorQUICConnectionInvalidConfig); + ).rejects.not.toThrow(errors.ErrorQUICConnectionConfigInvalid); // Not setting an interval doesn't cause a problem either await expect( QUICClient.createQUICClient({ @@ -248,7 +246,7 @@ describe(QUICClient.name, () => { port: 56666, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -256,7 +254,7 @@ describe(QUICClient.name, () => { verifyPeer: false, }, }), - ).rejects.not.toThrow(errors.ErrorQUICConnectionInvalidConfig); + ).rejects.not.toThrow(errors.ErrorQUICConnectionConfigInvalid); }); test('client times out with ctx timer while starting', async () => { // QUICClient repeatedly dials until the connection timeout @@ -267,7 +265,7 @@ describe(QUICClient.name, () => { port: 56666, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -278,7 +276,7 @@ describe(QUICClient.name, () => { }, { timer: 100 }, ), - ).rejects.toThrow(errors.ErrorQUICClientCreateTimeOut); + ).rejects.toThrow(errors.ErrorQUICClientCreateTimeout); }); test('client times out with ctx signal while starting', async () => { // QUICClient repeatedly dials until the connection timeout @@ -289,7 +287,7 @@ describe(QUICClient.name, () => { port: 56666, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -307,20 +305,20 @@ describe(QUICClient.name, () => { }); describe.each(types)('TLS rotation with %s', (type) => { test('existing connections config is unchanged and still function', async () => { - const tlsConfig1 = await testsUtils.generateConfig(type); - const tlsConfig2 = await testsUtils.generateConfig(type); + const tlsConfig1 = await testsUtils.generateTLSConfig(type); + const tlsConfig2 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); await server.start({ host: localhost, }); @@ -329,19 +327,21 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: true, - verifyAllowFail: true, + verifyCallback: async () => { + return undefined; + }, }, }); - testsUtils.extractSocket(client1, sockets); + socketCleanMethods.extractSocket(client1); const peerCertChainInitial = client1.connection.conn.peerCertChain(); server.updateConfig({ - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, }); // The existing connection's certs should be unchanged const peerCertChainNew = client1.connection.conn.peerCertChain(); @@ -352,20 +352,20 @@ describe(QUICClient.name, () => { await server.stop(); }); test('new connections use new config', async () => { - const tlsConfig1 = await testsUtils.generateConfig(type); - const tlsConfig2 = await testsUtils.generateConfig(type); + const tlsConfig1 = await testsUtils.generateTLSConfig(type); + const tlsConfig2 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); await server.start({ host: localhost, }); @@ -374,19 +374,21 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: true, - verifyAllowFail: true, + verifyCallback: async () => { + return undefined; + }, }, }); - testsUtils.extractSocket(client1, sockets); + socketCleanMethods.extractSocket(client1); const peerCertChainInitial = client1.connection.conn.peerCertChain(); server.updateConfig({ - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, }); // Starting a new connection has a different peerCertChain const client2 = await QUICClient.createQUICClient({ @@ -394,15 +396,17 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: true, - verifyAllowFail: true, + verifyCallback: async () => { + return undefined; + }, }, }); - testsUtils.extractSocket(client2, sockets); + socketCleanMethods.extractSocket(client2); const peerCertChainNew = client2.connection.conn.peerCertChain(); expect(peerCertChainNew![0].toString()).not.toStrictEqual( peerCertChainInitial![0].toString(), @@ -414,23 +418,23 @@ describe(QUICClient.name, () => { }); describe.each(types)('graceful tls handshake with %s certs', (type) => { test('server verification succeeds', async () => { - const tlsConfigs = await testsUtils.generateConfig(type); + const tlsConfigs = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', + events.EventQUICServerConnection.name, handleConnectionEventProm.resolveP, ); await server.start({ @@ -442,38 +446,38 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: true, - ca: tlsConfigs.ca, + ca: tlsConfigs.caCertPEM, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); await handleConnectionEventProm.p; await client.destroy(); await server.stop(); }); test('client verification succeeds', async () => { - const tlsConfigs1 = await testsUtils.generateConfig(type); - const tlsConfigs2 = await testsUtils.generateConfig(type); + const tlsConfigs1 = await testsUtils.generateTLSConfig(type); + const tlsConfigs2 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs1.key, - cert: tlsConfigs1.cert, + key: tlsConfigs1.leafKeyPairPEM.privateKey, + cert: tlsConfigs1.leafCertPEM, verifyPeer: true, - ca: tlsConfigs2.ca, + ca: tlsConfigs2.caCertPEM, }, }); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', + events.EventQUICServerConnection.name, handleConnectionEventProm.resolveP, ); await server.start({ @@ -485,12 +489,12 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { - key: tlsConfigs2.key, - cert: tlsConfigs2.cert, + key: tlsConfigs2.leafKeyPairPEM.privateKey, + cert: tlsConfigs2.leafCertPEM, verifyPeer: false, }, }); @@ -498,25 +502,25 @@ describe(QUICClient.name, () => { await server.stop(); }); test('client and server verification succeeds', async () => { - const tlsConfigs1 = await testsUtils.generateConfig(type); - const tlsConfigs2 = await testsUtils.generateConfig(type); + const tlsConfigs1 = await testsUtils.generateTLSConfig(type); + const tlsConfigs2 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs1.key, - cert: tlsConfigs1.cert, - ca: tlsConfigs2.ca, + key: tlsConfigs1.leafKeyPairPEM.privateKey, + cert: tlsConfigs1.leafCertPEM, + ca: tlsConfigs2.caCertPEM, verifyPeer: true, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', + events.EventQUICServerConnection.name, handleConnectionEventProm.resolveP, ); await server.start({ @@ -528,36 +532,36 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { - key: tlsConfigs2.key, - cert: tlsConfigs2.cert, - ca: tlsConfigs1.ca, + key: tlsConfigs2.leafKeyPairPEM.privateKey, + cert: tlsConfigs2.leafCertPEM, + ca: tlsConfigs1.caCertPEM, verifyPeer: true, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); await handleConnectionEventProm.p; await client.destroy(); await server.stop(); }); test('graceful failure verifying server', async () => { - const tlsConfigs1 = await testsUtils.generateConfig(type); + const tlsConfigs1 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs1.key, - cert: tlsConfigs1.cert, + key: tlsConfigs1.leafKeyPairPEM.privateKey, + cert: tlsConfigs1.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); await server.start({ host: localhost, }); @@ -568,7 +572,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -579,59 +583,69 @@ describe(QUICClient.name, () => { await server.stop(); }); test('graceful failure verifying client', async () => { - const tlsConfigs1 = await testsUtils.generateConfig(type); - const tlsConfigs2 = await testsUtils.generateConfig(type); + const tlsConfigs1 = await testsUtils.generateTLSConfig(type); + const tlsConfigs2 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs1.key, - cert: tlsConfigs1.cert, + key: tlsConfigs1.leafKeyPairPEM.privateKey, + cert: tlsConfigs1.leafCertPEM, verifyPeer: true, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); await server.start({ host: localhost, }); - // Connection should fail - await expect( - QUICClient.createQUICClient({ - host: localhost, - port: server.port, - localHost: localhost, - crypto: { - ops: clientCrypto, - }, - logger: logger.getChild(QUICClient.name), - config: { - key: tlsConfigs2.key, - cert: tlsConfigs2.cert, - verifyPeer: false, - }, - }), - ).toReject(); + // Connection succeeds but peer will reject shortly after + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCryptoOps, + }, + logger: logger.getChild(QUICClient.name), + config: { + key: tlsConfigs2.leafKeyPairPEM.privateKey, + cert: tlsConfigs2.leafCertPEM, + verifyPeer: false, + }, + }); + + // Verification by peer happens after connection is securely established and started + const clientConnectionErrorProm = promise(); + client.connection.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + clientConnectionErrorProm.rejectP(evt.detail), + ); + await expect(clientConnectionErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionPeerTLS, + ); + await server.stop(); }); test('graceful failure verifying client and server', async () => { - const tlsConfigs1 = await testsUtils.generateConfig(type); - const tlsConfigs2 = await testsUtils.generateConfig(type); + const tlsConfigs1 = await testsUtils.generateTLSConfig(type); + const tlsConfigs2 = await testsUtils.generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs1.key, - cert: tlsConfigs1.cert, + key: tlsConfigs1.leafKeyPairPEM.privateKey, + cert: tlsConfigs1.leafCertPEM, verifyPeer: true, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); await server.start({ host: localhost, }); @@ -642,12 +656,12 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { - key: tlsConfigs2.key, - cert: tlsConfigs2.cert, + key: tlsConfigs2.leafKeyPairPEM.privateKey, + cert: tlsConfigs2.leafCertPEM, verifyPeer: true, }, }), @@ -664,7 +678,7 @@ describe(QUICClient.name, () => { fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), ], async (data, messages) => { - const tlsConfig = await testsUtils.generateConfig('RSA'); + const tlsConfig = await testsUtils.generateTLSConfig('RSA'); const socket = new QUICSocket({ logger: logger.getChild('socket'), }); @@ -674,21 +688,21 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, socket, }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); + socketCleanMethods.extractSocket(server); + const connectionEventProm = promise(); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -699,20 +713,20 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamProm = stream.readable.pipeTo(stream.writable); serverStreamProms.push(streamProm); @@ -725,7 +739,7 @@ describe(QUICClient.name, () => { while (running) { await socket.send( data[count % data.length], - client.port, + client.localPort, '127.0.0.1', ); await sleep(5); @@ -734,7 +748,7 @@ describe(QUICClient.name, () => { })(); // We want to check that things function fine between bad data const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); await Promise.all([ (async () => { // Write data @@ -773,7 +787,7 @@ describe(QUICClient.name, () => { fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), ], async (data, messages) => { - const tlsConfig = await testsUtils.generateConfig('RSA'); + const tlsConfig = await testsUtils.generateTLSConfig('RSA'); const socket = new QUICSocket({ logger: logger.getChild('socket'), }); @@ -783,20 +797,20 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); + socketCleanMethods.extractSocket(server); + const connectionEventProm = promise(); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -807,20 +821,20 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamProm = stream.readable.pipeTo(stream.writable); serverStreamProms.push(streamProm); @@ -833,7 +847,7 @@ describe(QUICClient.name, () => { while (running) { await socket.send( data[count % data.length], - client.port, + client.localPort, '127.0.0.1', ); await sleep(5); @@ -842,7 +856,7 @@ describe(QUICClient.name, () => { })(); // We want to check that things function fine between bad data const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); await Promise.all([ (async () => { // Write data @@ -881,7 +895,7 @@ describe(QUICClient.name, () => { fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), ], async (data, messages) => { - const tlsConfig = await testsUtils.generateConfig('RSA'); + const tlsConfig = await testsUtils.generateTLSConfig('RSA'); const socket = new QUICSocket({ logger: logger.getChild('socket'), }); @@ -891,20 +905,20 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); + socketCleanMethods.extractSocket(server); + const connectionEventProm = promise(); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -915,20 +929,20 @@ describe(QUICClient.name, () => { port: server.port, socket, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamProm = stream.readable.pipeTo(stream.writable); serverStreamProms.push(streamProm); @@ -950,7 +964,7 @@ describe(QUICClient.name, () => { })(); // We want to check that things function fine between bad data const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); await Promise.all([ (async () => { // Write data @@ -989,7 +1003,7 @@ describe(QUICClient.name, () => { fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), ], async (data, messages) => { - const tlsConfig = await testsUtils.generateConfig('RSA'); + const tlsConfig = await testsUtils.generateTLSConfig('RSA'); const socket = new QUICSocket({ logger: logger.getChild('socket'), }); @@ -999,20 +1013,20 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); + socketCleanMethods.extractSocket(server); + const connectionEventProm = promise(); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ @@ -1023,20 +1037,20 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamProm = stream.readable.pipeTo(stream.writable); serverStreamProms.push(streamProm); @@ -1058,7 +1072,7 @@ describe(QUICClient.name, () => { })(); // We want to check that things function fine between bad data const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); await Promise.all([ (async () => { // Write data @@ -1094,27 +1108,27 @@ describe(QUICClient.name, () => { describe('keepalive', () => { let tlsConfig: TLSConfigs; beforeEach(async () => { - tlsConfig = await testsUtils.generateConfig('RSA'); + tlsConfig = await testsUtils.generateTLSConfig('RSA'); }); test('connection can time out on client', async () => { const connectionEventProm = promise(); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, maxIdleTimeout: 1000, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e.detail), ); await server.start({ @@ -1125,7 +1139,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1133,15 +1147,15 @@ describe(QUICClient.name, () => { maxIdleTimeout: 100, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); // Setting no keepalive should cause the connection to time out // It has cleaned up due to timeout const clientConnection = client.connection; const clientTimeoutProm = promise(); clientConnection.addEventListener( - 'connectionError', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + events.EventQUICConnectionError.name, + (event: events.EventQUICConnectionError) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeout) { clientTimeoutProm.resolveP(); } }, @@ -1161,20 +1175,20 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, maxIdleTimeout: 100, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e.detail), ); await server.start({ @@ -1185,7 +1199,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1193,16 +1207,16 @@ describe(QUICClient.name, () => { maxIdleTimeout: 1000, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); // Setting no keepalive should cause the connection to time out // It has cleaned up due to timeout const clientConnection = client.connection; const serverConnection = await connectionEventProm.p; const serverTimeoutProm = promise(); serverConnection.addEventListener( - 'connectionError', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => { + if (evt.detail instanceof errors.ErrorQUICConnectionIdleTimeout) { serverTimeoutProm.resolveP(); } }, @@ -1221,20 +1235,20 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, maxIdleTimeout: 20000, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e.detail), ); await server.start({ @@ -1245,7 +1259,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1254,15 +1268,15 @@ describe(QUICClient.name, () => { keepAliveIntervalTime: 50, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); // Setting no keepalive should cause the connection to time out // It has cleaned up due to timeout const clientConnection = client.connection; const clientTimeoutProm = promise(); clientConnection.addEventListener( - 'connectionError', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionError) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeout) { clientTimeoutProm.resolveP(); } }, @@ -1283,21 +1297,21 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, maxIdleTimeout: 100, keepAliveIntervalTime: 50, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e.detail), ); await server.start({ @@ -1308,7 +1322,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1316,15 +1330,15 @@ describe(QUICClient.name, () => { maxIdleTimeout: 20000, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); // Setting no keepalive should cause the connection to time out // It has cleaned up due to timeout const serverConnection = await connectionEventProm.p; const serverTimeoutProm = promise(); serverConnection.addEventListener( - 'connectionError', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionError) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeout) { serverTimeoutProm.resolveP(); } }, @@ -1344,20 +1358,20 @@ describe(QUICClient.name, () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, maxIdleTimeout: 100, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e.detail), ); await server.start({ @@ -1368,7 +1382,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1377,15 +1391,15 @@ describe(QUICClient.name, () => { keepAliveIntervalTime: 50, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); // Setting no keepalive should cause the connection to time out // It has cleaned up due to timeout const serverConnection = await connectionEventProm.p; const serverTimeoutProm = promise(); serverConnection.addEventListener( - 'connectionError', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionError) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeout) { serverTimeoutProm.resolveP(); } }, @@ -1406,7 +1420,7 @@ describe(QUICClient.name, () => { port: 54444, localHost: '::', crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1416,77 +1430,77 @@ describe(QUICClient.name, () => { }, }); await expect(clientProm).rejects.toThrow( - errors.ErrorQUICConnectionStartTimeOut, + errors.ErrorQUICConnectionIdleTimeout, ); }); }); describe.each(types)('custom TLS verification with %s', (type) => { test('server succeeds custom verification', async () => { - const tlsConfigs = await generateConfig(type); + const tlsConfigs = await generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', + events.EventQUICServerConnection.name, handleConnectionEventProm.resolveP, ); await server.start({ host: localhost, }); // Connection should succeed - const verifyProm = promise | undefined>(); + const verifyProm = promise | undefined>(); const client = await QUICClient.createQUICClient({ host: localhost, port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: true, - verifyAllowFail: true, - }, - verifyCallback: async (certs) => { - verifyProm.resolveP(certs); + verifyCallback: async (certs) => { + verifyProm.resolveP(certs); + return undefined; + }, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); await handleConnectionEventProm.p; await expect(verifyProm.p).toResolve(); await client.destroy(); await server.stop(); }); test('server fails custom verification', async () => { - const tlsConfigs = await generateConfig(type); + const tlsConfigs = await generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', - (event: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (event: events.EventQUICServerConnection) => handleConnectionEventProm.resolveP(event.detail), ); await server.start({ @@ -1498,57 +1512,56 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: true, - verifyAllowFail: true, - }, - verifyCallback: () => { - throw Error('SOME ERROR'); + verifyCallback: async () => { + return CryptoError.BadCertificate; + }, }, }); clientProm.catch(() => {}); - // Server connection is never emitted - await Promise.race([ - handleConnectionEventProm.p.then(() => { - throw Error('Server connection should not be emitted'); - }), - // Allow some time - sleep(200), - ]); - - await expect(clientProm).rejects.toThrow( - errors.ErrorQUICConnectionInternal, + // Verification by peer happens after connection is securely established and started + const serverConn = await handleConnectionEventProm.p; + const serverErrorProm = promise(); + serverConn.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + serverErrorProm.rejectP(evt.detail), + ); + await expect(serverErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionPeerTLS, ); + await expect(clientProm).rejects.toThrow(errors.ErrorQUICConnectionLocal); await server.stop(); }); test('client succeeds custom verification', async () => { - const tlsConfigs = await generateConfig(type); - const verifyProm = promise | undefined>(); + const tlsConfigs = await generateTLSConfig(type); + const verifyProm = promise | undefined>(); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, verifyPeer: true, - verifyAllowFail: true, - }, - verifyCallback: async (certs) => { - verifyProm.resolveP(certs); + verifyCallback: async (certs) => { + verifyProm.resolveP(certs); + return undefined; + }, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', + events.EventQUICServerConnection.name, handleConnectionEventProm.resolveP, ); await server.start({ @@ -1560,44 +1573,43 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); await handleConnectionEventProm.p; await expect(verifyProm.p).toResolve(); await client.destroy(); await server.stop(); }); test('client fails custom verification', async () => { - const tlsConfigs = await generateConfig(type); + const tlsConfigs = await generateTLSConfig(type); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, verifyPeer: true, - verifyAllowFail: true, - }, - verifyCallback: () => { - throw Error('SOME ERROR'); + verifyCallback: async () => { + return CryptoError.BadCertificate; + }, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); const handleConnectionEventProm = promise(); server.addEventListener( - 'serverConnection', - (event: events.QUICServerConnectionEvent) => + events.EventQUICServerConnection.name, + (event: events.EventQUICServerConnection) => handleConnectionEventProm.resolveP(event.detail), ); await server.start({ @@ -1605,21 +1617,31 @@ describe(QUICClient.name, () => { port: 55555, }); // Connection should fail - const clientProm = QUICClient.createQUICClient({ + const client = await QUICClient.createQUICClient({ host: localhost, port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { - key: tlsConfigs.key, - cert: tlsConfigs.cert, + key: tlsConfigs.leafKeyPairPEM.privateKey, + cert: tlsConfigs.leafCertPEM, verifyPeer: false, }, }); - clientProm.catch(() => {}); + + // Verification by peer happens after connection is securely established and started + const clientConnectionErrorProm = promise(); + client.connection.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + clientConnectionErrorProm.rejectP(evt.detail), + ); + await expect(clientConnectionErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionPeerTLS, + ); // Server connection is never emitted await Promise.race([ @@ -1630,33 +1652,29 @@ describe(QUICClient.name, () => { sleep(200), ]); - await expect(clientProm).rejects.toThrow( - errors.ErrorQUICConnectionInternal, - ); - await server.stop(); }); }); test('Connections are established and secured quickly', async () => { - const tlsConfigServer = await testsUtils.generateConfig(defaultType); + const tlsConfigServer = await testsUtils.generateTLSConfig(defaultType); - const connectionEventProm = promise(); + const connectionEventProm = promise(); const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfigServer.key, - cert: tlsConfigServer.cert, + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -1672,7 +1690,7 @@ describe(QUICClient.name, () => { port: server.port, localHost: localhost, crypto: { - ops: clientCrypto, + ops: clientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -1681,9 +1699,119 @@ describe(QUICClient.name, () => { }, { timer: 500 }, ); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); await connectionEventProm.p; await client.destroy({ force: true }); await server.stop({ force: true }); }); + test('socket stopping first triggers client destruction', async () => { + const tlsConfigServer = await testsUtils.generateTLSConfig(defaultType); + + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCryptoOps, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, + verifyPeer: false, + maxIdleTimeout: 200, + }, + }); + socketCleanMethods.extractSocket(server); + server.addEventListener( + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: localhost, + port: 55555, + }); + // If the server is slow to respond then this will time out. + // Then main cause of this was the server not processing the initial packet + // that creates the `QUICConnection`, as a result, the whole creation waited + // an extra 1 second for the client to retry the initial packet. + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCryptoOps, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + socketCleanMethods.extractSocket(client); + + const serverConnection = await connectionEventProm.p; + // Handling server connection error event + const serverConnectionErrorProm = promise(); + serverConnection.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + serverConnectionErrorProm.rejectP(evt.detail), + { once: true }, + ); + + // Handling client connection error event + const clientConnectionErrorProm = promise(); + client.connection.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + clientConnectionErrorProm.rejectP(evt.detail), + { once: true }, + ); + + // Handling client destroy event + const clientConnectionStoppedProm = promise(); + client.connection.addEventListener( + events.EventQUICConnectionStopped.name, + () => clientConnectionStoppedProm.resolveP(), + { once: true }, + ); + + // Handling client error event + const clientErrorProm = promise(); + client.addEventListener( + events.EventQUICClientError.name, + (evt: events.EventQUICClientError) => clientErrorProm.rejectP(evt.detail), + { once: true }, + ); + + // Handling client destroy event + const clientDestroyedProm = promise(); + client.addEventListener( + events.EventQUICClientDestroyed.name, + () => clientDestroyedProm.resolveP(), + { once: true }, + ); + + // @ts-ignore: kidnap protected property + const clientSocket = client.socket; + await clientSocket.stop({ force: true }); + + // Socket failure triggers client connection local failure + await expect(clientConnectionErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionLocal, + ); + await expect(clientErrorProm.p).rejects.toThrow( + errors.ErrorQUICClientSocketNotRunning, + ); + await clientDestroyedProm.p; + await clientConnectionStoppedProm.p; + + // Socket failure will not trigger any close frame since transport has failed so server connection will time out + await expect(serverConnectionErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionIdleTimeout, + ); + + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); }); diff --git a/tests/QUICServer.test.ts b/tests/QUICServer.test.ts index 1c9198c8..e3ea40d7 100644 --- a/tests/QUICServer.test.ts +++ b/tests/QUICServer.test.ts @@ -1,8 +1,14 @@ import type { X509Certificate } from '@peculiar/x509'; -import type { Host, ServerCrypto } from '@/types'; +import type { Host, ServerCryptoOps } from '@/types'; +import type QUICConnection from '@/QUICConnection'; +import type { ClientCryptoOps } from '@/types'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; import QUICServer from '@/QUICServer'; import * as utils from '@/utils'; +import { promise } from '@/utils'; +import * as events from '@/events'; +import QUICClient from '@/QUICClient'; +import * as errors from '@/errors'; import * as testsUtils from './utils'; describe(QUICServer.name, () => { @@ -71,21 +77,26 @@ describe(QUICServer.name, () => { certEd25519PEM = testsUtils.certToPEM(certEd25519); }); // This has to be setup asynchronously due to key generation - let serverCrypto: ServerCrypto; + let serverCryptoOps: ServerCryptoOps; let key: ArrayBuffer; + let socketCleanMethods: ReturnType; beforeEach(async () => { key = await testsUtils.generateKeyHMAC(); - serverCrypto = { + serverCryptoOps = { sign: testsUtils.signHMAC, verify: testsUtils.verifyHMAC, }; + socketCleanMethods = testsUtils.socketCleanupFactory(); + }); + afterEach(async () => { + await socketCleanMethods.stopSockets(); }); describe('start and stop', () => { test('with RSA', async () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairRSAPEM.privateKey, @@ -93,6 +104,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start(); // Default to dual-stack expect(quicServer.host).toBe('::'); @@ -103,7 +115,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairECDSAPEM.privateKey, @@ -111,6 +123,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start(); // Default to dual-stack expect(quicServer.host).toBe('::'); @@ -121,7 +134,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -129,6 +142,31 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); + await quicServer.start(); + // Default to dual-stack + expect(quicServer.host).toBe('::'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('repeated stop and start', async () => { + const quicServer = new QUICServer({ + crypto: { + key, + ops: serverCryptoOps, + }, + config: { + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }, + logger: logger.getChild('QUICServer'), + }); + socketCleanMethods.extractSocket(quicServer); + await quicServer.start(); + // Default to dual-stack + expect(quicServer.host).toBe('::'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); await quicServer.start(); // Default to dual-stack expect(quicServer.host).toBe('::'); @@ -141,7 +179,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -149,6 +187,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start({ host: '127.0.0.1', }); @@ -160,7 +199,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -168,6 +207,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start({ host: '::1', }); @@ -179,7 +219,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -187,6 +227,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start({ host: '::', }); @@ -200,7 +241,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -208,6 +249,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start({ host: '::ffff:127.0.0.1', }); @@ -226,7 +268,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -234,6 +276,7 @@ describe(QUICServer.name, () => { }, logger: logger.getChild('QUICServer'), }); + socketCleanMethods.extractSocket(quicServer); await quicServer.start({ host: 'localhost', }); @@ -247,7 +290,7 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: serverCryptoOps, }, config: { key: keyPairEd25519PEM.privateKey, @@ -264,13 +307,14 @@ describe(QUICServer.name, () => { await quicServer.stop(); }); }); + // TODO: what is this? re-enable? // Describe.only('connection bootstrap', () => { // // Test without peer verification // test.only('', async () => { // const quicServer = new QUICServer({ // crypto: { // key, - // ops: serverCrypto, + // ops: serverCryptoOps, // }, // config: { // key: keyPairECDSAPEM.privateKey, @@ -399,4 +443,117 @@ describe(QUICServer.name, () => { // Test hole punching, there's an initiation function // We can make it start doing this, but technically it's the socket's duty to do this // not just the server side + test('socket stopping first triggers client destruction', async () => { + const tlsConfigServer = await testsUtils.generateTLSConfig('RSA'); + + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCryptoOps, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, + verifyPeer: false, + maxIdleTimeout: 200, + }, + }); + socketCleanMethods.extractSocket(server); + server.addEventListener( + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: '127.0.0.1', + port: 55555, + }); + const clientCryptoOps: ClientCryptoOps = { + randomBytes: testsUtils.randomBytes, + }; + // If the server is slow to respond then this will time out. + // Then main cause of this was the server not processing the initial packet + // that creates the `QUICConnection`, as a result, the whole creation waited + // an extra 1 second for the client to retry the initial packet. + const client = await QUICClient.createQUICClient({ + host: '127.0.0.1', + port: server.port, + localHost: '127.0.0.1', + crypto: { + ops: clientCryptoOps, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + socketCleanMethods.extractSocket(client); + + // Handling client connection error event + const clientConnectionErrorProm = promise(); + client.connection.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + clientConnectionErrorProm.rejectP(evt.detail), + { once: true }, + ); + + const serverConnection = await connectionEventProm.p; + // Handling server connection error event + const serverConnectionErrorProm = promise(); + serverConnection.addEventListener( + events.EventQUICConnectionError.name, + (evt: events.EventQUICConnectionError) => + serverConnectionErrorProm.rejectP(evt.detail), + { once: true }, + ); + + // Handling server connection stop event + const serverConnectionStoppedProm = promise(); + client.connection.addEventListener( + events.EventQUICConnectionStopped.name, + () => serverConnectionStoppedProm.resolveP(), + { once: true }, + ); + + // Handling server error event + const serverErrorProm = promise(); + server.addEventListener( + events.EventQUICServerError.name, + (evt: events.EventQUICServerError) => serverErrorProm.rejectP(evt.detail), + { once: true }, + ); + + // Handling client destroy event + const serverStoppedProm = promise(); + server.addEventListener( + events.EventQUICServerStopped.name, + () => serverStoppedProm.resolveP(), + { once: true }, + ); + + // @ts-ignore: kidnap protected property + const serverSocket = server.socket; + await serverSocket.stop({ force: true }); + + // Socket failure triggers server connection local failure + await expect(serverConnectionErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionLocal, + ); + await expect(serverErrorProm.p).rejects.toThrow( + errors.ErrorQUICServerSocketNotRunning, + ); + await serverStoppedProm.p; + await serverConnectionStoppedProm.p; + + // Socket failure will not trigger any close frame since transport has failed so client connection will time out + await expect(clientConnectionErrorProm.p).rejects.toThrow( + errors.ErrorQUICConnectionIdleTimeout, + ); + + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); }); diff --git a/tests/QUICSocket.test.ts b/tests/QUICSocket.test.ts index 59206389..fb69a243 100644 --- a/tests/QUICSocket.test.ts +++ b/tests/QUICSocket.test.ts @@ -1,21 +1,28 @@ import type QUICConnection from '@/QUICConnection'; +import type QUICServer from '@/QUICServer'; import dgram from 'dgram'; +import { testProp } from '@fast-check/jest'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { running } from '@matrixai/async-init'; import QUICSocket from '@/QUICSocket'; +import QUICConnectionId from '@/QUICConnectionId'; +import { quiche } from '@/native'; import * as utils from '@/utils'; import * as errors from '@/errors'; -import QUICConnectionId from '@/QUICConnectionId'; +import * as events from '@/events'; +import * as testsUtils from './utils'; describe(QUICSocket.name, () => { const logger = new Logger(`${QUICSocket.name} Test`, LogLevel.WARN, [ new StreamHandler(), ]); // This has to be setup asynchronously due to key generation + // These socket fixtures will be used to test against the `QUICSocket` let ipv4Socket: dgram.Socket; let ipv6Socket: dgram.Socket; let dualStackSocket: dgram.Socket; let ipv4SocketBind: (port: number, host: string) => Promise; - let _ipv4SocketSend: (...params: Array) => Promise; + let ipv4SocketSend: (...params: Array) => Promise; let ipv4SocketClose: () => Promise; let ipv4SocketPort: number; // Handle IPv4 messages @@ -28,7 +35,7 @@ describe(QUICSocket.name, () => { ipv4SocketMessageResolveP = resolveP; }; let ipv6SocketBind: (port: number, host: string) => Promise; - let _ipv6SocketSend: (...params: Array) => Promise; + let ipv6SocketSend: (...params: Array) => Promise; let ipv6SocketClose: () => Promise; let ipv6SocketPort: number; // Handle IPv6 messages @@ -41,7 +48,7 @@ describe(QUICSocket.name, () => { ipv6SocketMessageResolveP = resolveP; }; let dualStackSocketBind: (port: number, host: string) => Promise; - let _dualStackSocketSend: (...params: Array) => Promise; + let dualStackSocketSend: (...params: Array) => Promise; let dualStackSocketClose: () => Promise; let dualStackSocketPort: number; // Handle dual stack messages @@ -69,21 +76,21 @@ describe(QUICSocket.name, () => { ipv6Only: false, }); ipv4SocketBind = utils.promisify(ipv4Socket.bind).bind(ipv4Socket); - _ipv4SocketSend = utils.promisify(ipv4Socket.send).bind(ipv4Socket); + ipv4SocketSend = utils.promisify(ipv4Socket.send).bind(ipv4Socket); ipv4SocketClose = utils.promisify(ipv4Socket.close).bind(ipv4Socket); ipv6SocketBind = utils.promisify(ipv6Socket.bind).bind(ipv6Socket); - _ipv6SocketSend = utils.promisify(ipv6Socket.send).bind(ipv6Socket); + ipv6SocketSend = utils.promisify(ipv6Socket.send).bind(ipv6Socket); ipv6SocketClose = utils.promisify(ipv6Socket.close).bind(ipv6Socket); dualStackSocketBind = utils .promisify(dualStackSocket.bind) .bind(dualStackSocket); - _dualStackSocketSend = utils + dualStackSocketSend = utils .promisify(dualStackSocket.send) .bind(dualStackSocket); dualStackSocketClose = utils .promisify(dualStackSocket.close) .bind(dualStackSocket); - await ipv4SocketBind(57777, '127.0.0.1'); + await ipv4SocketBind(0, '127.0.0.1'); await ipv6SocketBind(0, '::1'); await dualStackSocketBind(0, '::'); ipv4SocketPort = ipv4Socket.address().port; @@ -101,6 +108,232 @@ describe(QUICSocket.name, () => { ipv6Socket.off('message', handleIPv6SocketMessage); dualStackSocket.off('message', handleDualStackSocketMessage); }); + test('cannot stop socket with active connections', async () => { + const socket = new QUICSocket({ + logger, + }); + await socket.start({ + host: '127.0.0.1', + }); + const connectionId = QUICConnectionId.fromBuffer( + Buffer.from('SomeRandomId'), + ); + // Mock connection, we only need the `type` property + const connection = { type: 'client' } as QUICConnection; + socket.connectionMap.set(connectionId, connection); + await expect(socket.stop()).rejects.toThrow( + errors.ErrorQUICSocketConnectionsActive, + ); + socket.connectionMap.delete(connectionId); + await expect(socket.stop()).toResolve(); + }); + test('force stop socket with active connections', async () => { + const socket = new QUICSocket({ + logger, + }); + await socket.start({ + host: '127.0.0.1', + }); + const connectionId = QUICConnectionId.fromBuffer( + Buffer.from('SomeRandomId'), + ); + // Mock connection, we only need the `type` property + const connection = { type: 'client' } as QUICConnection; + socket.connectionMap.set(connectionId, connection); + await expect(socket.stop({ force: true })).toResolve(); + }); + test('enabling ipv6 only prevents binding to ipv4 hosts', async () => { + const socket = new QUICSocket({ + logger, + }); + await expect( + socket.start({ + host: '127.0.0.1', + ipv6Only: true, + }), + ).rejects.toThrow(errors.ErrorQUICSocketInvalidBindAddress); + await socket.stop(); + }); + test('disabling ipv6 only does not prevent binding to ipv6 hosts', async () => { + const socket = new QUICSocket({ + logger, + }); + await socket.start({ + host: '::1', + ipv6Only: false, + }); + await socket.stop(); + }); + test('ipv4 wildcard to ipv4 succeeds', async () => { + const socket = new QUICSocket({ + logger, + }); + await socket.start({ + host: '0.0.0.0', + }); + const msg = Buffer.from('Hello World'); + await socket.send(msg, ipv4SocketPort, '127.0.0.1'); + await expect(ipv4SocketMessageP).resolves.toEqual([ + msg, + { + address: '127.0.0.1', + family: 'IPv4', + port: socket.port, + size: msg.byteLength, + }, + ]); + await socket.stop(); + }); + test('ipv6 wildcard to ipv6 succeeds', async () => { + const socket = new QUICSocket({ + logger, + }); + await socket.start({ + host: '::0', + }); + const msg = Buffer.from('Hello World'); + await socket.send(msg, ipv6SocketPort, '::1'); + await expect(ipv6SocketMessageP).resolves.toEqual([ + msg, + { + address: '::1', + family: 'IPv6', + port: socket.port, + size: msg.byteLength, + }, + ]); + await socket.stop(); + }); + test('failed send is a caller error and does not result in domain error', async () => { + const socket = new QUICSocket({ + logger, + }); + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + await socket.start({ + host: '::', + }); + const msg = Buffer.from('hello world'); + await expect( + // @ts-ignore - attempting to throw a `TypeError` + socket.send(msg), + ).rejects.toThrow(TypeError); + // This should throw a `RangeError` which comes from Node, but due to Jest VM isolation + await expect(socket.send(msg, 0, '::1')).rejects.toHaveProperty( + 'name', + 'RangeError', + ); + await expect(socket.send(msg, ipv4SocketPort, '0.0.0.0')).rejects.toThrow( + errors.ErrorQUICSocketInvalidSendAddress, + ); + expect(handleEventQUICSocketError).not.toHaveBeenCalled(); + await socket.stop(); + expect(handleEventQUICSocketError).not.toHaveBeenCalled(); + }); + describe('events', () => { + test('start and stop event lifecycle', async () => { + const socket = new QUICSocket({ + logger, + }); + const handleEventQUICSocketStart = jest.fn(); + socket.addEventListener( + events.EventQUICSocketStart.name, + handleEventQUICSocketStart, + ); + const handleEventQUICSocketStarted = jest.fn(); + socket.addEventListener( + events.EventQUICSocketStarted.name, + handleEventQUICSocketStarted, + ); + const handleEventQUICSocketStop = jest.fn(); + socket.addEventListener( + events.EventQUICSocketStop.name, + handleEventQUICSocketStop, + ); + const handleEventQUICSocketStopped = jest.fn(); + socket.addEventListener( + events.EventQUICSocketStopped.name, + handleEventQUICSocketStopped, + ); + const handleEventQUICSocketClose = jest.fn(); + socket.addEventListener( + events.EventQUICSocketClose.name, + handleEventQUICSocketClose, + ); + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + const startP = socket.start({ + host: '127.0.0.1', + }); + await testsUtils.sleep(0); + expect(handleEventQUICSocketStart).toHaveBeenCalled(); + await startP; + expect(handleEventQUICSocketStarted).toHaveBeenCalled(); + const stopP = socket.stop(); + await testsUtils.sleep(0); + expect(handleEventQUICSocketStop).toHaveBeenCalled(); + await stopP; + expect(handleEventQUICSocketClose).toHaveBeenCalled(); + expect(handleEventQUICSocketStopped).toHaveBeenCalled(); + expect(handleEventQUICSocketError).not.toHaveBeenCalled(); + }); + test('error and close event lifecycle', async () => { + // We expect error logs + const socketLogger = logger.getChild('abc'); + socketLogger.setLevel(LogLevel.SILENT); + const socket = new QUICSocket({ + logger: socketLogger, + }); + const handleEventQUICSocketStop = jest.fn(); + socket.addEventListener( + events.EventQUICSocketStop.name, + handleEventQUICSocketStop, + ); + const handleEventQUICSocketStopped = jest.fn(); + socket.addEventListener( + events.EventQUICSocketStopped.name, + handleEventQUICSocketStopped, + ); + const handleEventQUICSocketClose = jest.fn(); + socket.addEventListener( + events.EventQUICSocketClose.name, + handleEventQUICSocketClose, + ); + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + await socket.start({ + host: '127.0.0.1', + }); + socket.dispatchEvent( + new events.EventQUICSocketError({ + detail: new errors.ErrorQUICSocketInternal('Dummy Error'), + }), + ); + socket.dispatchEvent(new events.EventQUICSocketClose()); + // @ts-ignore: awaiting protected property + await socket.closedP; + expect(socket.closed).toBe(true); + expect(handleEventQUICSocketError).toHaveBeenCalledTimes(1); + expect(handleEventQUICSocketClose).toHaveBeenCalledTimes(1); + // Note that we may need to wait for the system to actually stop + await testsUtils.sleep(0); + expect(handleEventQUICSocketStop).toHaveBeenCalledTimes(1); + expect(handleEventQUICSocketStopped).toHaveBeenCalledTimes(1); + // It should be the case that the `socket` is already stopped + await socket.stop(); + expect(handleEventQUICSocketStop).toHaveBeenCalledTimes(1); + expect(handleEventQUICSocketStopped).toHaveBeenCalledTimes(1); + }); + }); describe('ipv4', () => { const socket = new QUICSocket({ logger, @@ -146,6 +379,28 @@ describe(QUICSocket.name, () => { }, ]); }); + test('to ipv4 mapped ipv6 succeeds', async () => { + await socket.send(msg, ipv4SocketPort, '::ffff:127.0.0.1'); + await expect(ipv4SocketMessageP).resolves.toEqual([ + msg, + { + address: '127.0.0.1', + family: 'IPv4', + port: socket.port, + size: msg.byteLength, + }, + ]); + await socket.send(msg, dualStackSocketPort, '::ffff:7f00:1'); + await expect(dualStackSocketMessageP).resolves.toEqual([ + msg, + { + address: '::ffff:127.0.0.1', + family: 'IPv6', + port: socket.port, + size: msg.byteLength, + }, + ]); + }); }); describe('ipv6', () => { const socket = new QUICSocket({ @@ -179,10 +434,6 @@ describe(QUICSocket.name, () => { await expect( socket.send(msg, ipv4SocketPort, '127.0.0.1'), ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); - // Does not work with IPv4 mapped IPv6 addresses - await expect( - socket.send(msg, ipv4SocketPort, '::ffff:127.0.0.1'), - ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); }); test('to dual stack succeeds', async () => { await socket.send(msg, dualStackSocketPort, '::1'); @@ -195,9 +446,13 @@ describe(QUICSocket.name, () => { size: msg.byteLength, }, ]); - // Does not work with IPv4 mapped IPv6 addresses + }); + test('to ipv4 mapped ipv6 fails', async () => { await expect( - socket.send(msg, dualStackSocketPort, '::ffff:127.0.0.1'), + socket.send(msg, ipv4SocketPort, '::ffff:127.0.0.1'), + ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + await expect( + socket.send(msg, dualStackSocketPort, '::ffff:7f00:1'), ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); }); }); @@ -238,7 +493,7 @@ describe(QUICSocket.name, () => { }, ]); }); - test('to dual stack succeds', async () => { + test('to dual stack succeeds', async () => { await socket.send(msg, dualStackSocketPort, '::1'); await expect(dualStackSocketMessageP).resolves.toEqual([ msg, @@ -250,6 +505,14 @@ describe(QUICSocket.name, () => { }, ]); }); + test('to ipv4 mapped ipv6 fails', async () => { + await expect( + socket.send(msg, ipv4SocketPort, '::ffff:127.0.0.1'), + ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + await expect( + socket.send(msg, dualStackSocketPort, '::ffff:7f00:1'), + ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + }); }); describe('dual stack', () => { const socket = new QUICSocket({ @@ -268,10 +531,16 @@ describe(QUICSocket.name, () => { expect(socket.type).toBe('ipv4&ipv6'); }); test('to ipv4 succeeds', async () => { - // Fail if send to IPv4 - await expect( - socket.send(msg, ipv4SocketPort, '127.0.0.1'), - ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + await socket.send(msg, ipv4SocketPort, '127.0.0.1'); + await expect(ipv4SocketMessageP).resolves.toEqual([ + msg, + { + address: '127.0.0.1', + family: 'IPv4', + port: socket.port, + size: msg.byteLength, + }, + ]); // Succeeds if sent with IPv4 mapped IPv6 address await socket.send(msg, ipv4SocketPort, '::ffff:127.0.0.1'); await expect(ipv4SocketMessageP).resolves.toEqual([ @@ -297,9 +566,16 @@ describe(QUICSocket.name, () => { ]); }); test('to dual stack succeeds', async () => { - await expect( - socket.send(msg, dualStackSocketPort, '127.0.0.1'), - ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + await socket.send(msg, dualStackSocketPort, '127.0.0.1'); + await expect(dualStackSocketMessageP).resolves.toEqual([ + msg, + { + address: '::ffff:127.0.0.1', + family: 'IPv6', + port: socket.port, + size: msg.byteLength, + }, + ]); await socket.send(msg, dualStackSocketPort, '::ffff:127.0.0.1'); await expect(dualStackSocketMessageP).resolves.toEqual([ msg, @@ -321,68 +597,28 @@ describe(QUICSocket.name, () => { }, ]); }); - }); - test('enabling ipv6 only prevents binding to ipv4 hosts', async () => { - const socket = new QUICSocket({ - logger, - }); - await expect( - socket.start({ - host: '127.0.0.1', - ipv6Only: true, - }), - ).rejects.toThrow(errors.ErrorQUICSocketInvalidBindAddress); - await socket.stop(); - }); - test('disabling ipv6 only does not prevent binding to ipv6 hosts', async () => { - const socket = new QUICSocket({ - logger, - }); - await socket.start({ - host: '::1', - ipv6Only: false, - }); - await socket.stop(); - }); - test('ipv4 wildcard to ipv4 succeeds', async () => { - const socket = new QUICSocket({ - logger, - }); - await socket.start({ - host: '0.0.0.0', - }); - const msg = Buffer.from('Hello World'); - await socket.send(msg, ipv4SocketPort, '127.0.0.1'); - await expect(ipv4SocketMessageP).resolves.toEqual([ - msg, - { - address: '127.0.0.1', - family: 'IPv4', - port: socket.port, - size: msg.byteLength, - }, - ]); - await socket.stop(); - }); - test('ipv6 wildcard to ipv6 succeeds', async () => { - const socket = new QUICSocket({ - logger, - }); - await socket.start({ - host: '::0', + test('to ipv4 mapped ipv6 succeeds', async () => { + await socket.send(msg, ipv4SocketPort, '::ffff:127.0.0.1'); + await expect(ipv4SocketMessageP).resolves.toEqual([ + msg, + { + address: '127.0.0.1', + family: 'IPv4', + port: socket.port, + size: msg.byteLength, + }, + ]); + await socket.send(msg, dualStackSocketPort, '::ffff:7f00:1'); + await expect(dualStackSocketMessageP).resolves.toEqual([ + msg, + { + address: '::ffff:127.0.0.1', + family: 'IPv6', + port: socket.port, + size: msg.byteLength, + }, + ]); }); - const msg = Buffer.from('Hello World'); - await socket.send(msg, ipv6SocketPort, '::1'); - await expect(ipv6SocketMessageP).resolves.toEqual([ - msg, - { - address: '::1', - family: 'IPv6', - port: socket.port, - size: msg.byteLength, - }, - ]); - await socket.stop(); }); describe('ipv4 mapped ipv6 - dotted decimal variant', () => { const socket = new QUICSocket({ @@ -400,10 +636,17 @@ describe(QUICSocket.name, () => { test('type will be `ipv4`', async () => { expect(socket.type).toBe('ipv4'); }); - test('to ipv4 fails', async () => { - await expect( - socket.send(msg, ipv4SocketPort, '127.0.0.1'), - ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + test('to ipv4 succeeds', async () => { + await socket.send(msg, ipv4SocketPort, '127.0.0.1'); + await expect(ipv4SocketMessageP).resolves.toEqual([ + msg, + { + address: '127.0.0.1', + family: 'IPv4', + port: socket.port, + size: msg.byteLength, + }, + ]); }); test('to ipv6 fails', async () => { await expect(socket.send(msg, ipv6SocketPort, '::1')).rejects.toThrow( @@ -421,12 +664,12 @@ describe(QUICSocket.name, () => { size: msg.byteLength, }, ]); - await socket.send(msg, ipv4SocketPort, '::ffff:7f00:1'); - await expect(ipv4SocketMessageP).resolves.toEqual([ + await socket.send(msg, dualStackSocketPort, '::ffff:7f00:1'); + await expect(dualStackSocketMessageP).resolves.toEqual([ msg, { - address: '127.0.0.1', - family: 'IPv4', + address: '::ffff:127.0.0.1', + family: 'IPv6', port: socket.port, size: msg.byteLength, }, @@ -451,10 +694,17 @@ describe(QUICSocket.name, () => { // Node dgram will resolve to dotted decimal variant expect(socket.host).toBe('::ffff:127.0.0.1'); }); - test('to ipv4 fails', async () => { - await expect( - socket.send(msg, ipv4SocketPort, '127.0.0.1'), - ).rejects.toThrow(errors.ErrorQUICSocketInvalidSendAddress); + test('to ipv4 succeeds', async () => { + await socket.send(msg, ipv4SocketPort, '127.0.0.1'); + await expect(ipv4SocketMessageP).resolves.toEqual([ + msg, + { + address: '127.0.0.1', + family: 'IPv4', + port: socket.port, + size: msg.byteLength, + }, + ]); }); test('to ipv6 fails', async () => { await expect(socket.send(msg, ipv6SocketPort, '::1')).rejects.toThrow( @@ -472,50 +722,239 @@ describe(QUICSocket.name, () => { size: msg.byteLength, }, ]); - await socket.send(msg, ipv4SocketPort, '::ffff:7f00:1'); - await expect(ipv4SocketMessageP).resolves.toEqual([ + await socket.send(msg, dualStackSocketPort, '::ffff:7f00:1'); + await expect(dualStackSocketMessageP).resolves.toEqual([ msg, { - address: '127.0.0.1', - family: 'IPv4', + address: '::ffff:127.0.0.1', + family: 'IPv6', port: socket.port, size: msg.byteLength, }, ]); }); }); - test('socket should throw if stopped with active connections', async () => { - const socket = new QUICSocket({ - logger, - }); - await socket.start({ - host: '127.0.0.1', - }); - const connectionId = QUICConnectionId.fromBuffer( - Buffer.from('SomeRandomId'), + describe('receiving datagrams', () => { + testProp( + 'random datagrams are discarded when there is no server', + [testsUtils.bufferArb({ minLength: 0, maxLength: 100 })], + async (message) => { + const socket = new QUICSocket({ + logger, + }); + // We have to spy on the arrow function property before it is registered + // Which meanst this spy must be created before the socket is started + const handleSocketMessageMock = jest.spyOn( + socket, + 'handleSocketMessage' as any, + ); + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + await socket.start({ + host: '127.0.0.1', + }); + // No server + socket.unsetServer(); + // These messages would be discarded + await ipv4SocketSend( + message, + 0, + message.byteLength, + socket.port, + socket.host, + ); + // Allow the event loop to flush the UDP socket + await testsUtils.sleep(0); + expect(handleSocketMessageMock).toHaveBeenCalledTimes(1); + expect(handleEventQUICSocketError).not.toHaveBeenCalled(); + await socket.stop(); + }, ); - socket.connectionMap.set(connectionId, { - type: 'client', - } as QUICConnection); - await expect(socket.stop()).rejects.toThrow( - errors.ErrorQUICSocketConnectionsActive, + testProp( + 'datagrams at >20 bytes are sometimes accepted for new connections', + [ + testsUtils.bufferArb({ + minLength: quiche.MAX_CONN_ID_LEN + 1, + maxLength: 100, + }), + ], + async (message) => { + const quicServer = { + acceptConnection: jest.fn().mockResolvedValue(undefined), + }; + const socket = new QUICSocket({ + logger, + }); + // We have to spy on the arrow function property before it is registered + // Which meanst this spy must be created before the socket is started + const { + p: handleSocketMessageMockP, + resolveP: resolveHandleSocketMessageMockP, + } = utils.promise(); + const handleSocketMessage = (socket as any).handleSocketMessage; + const handleSocketMessageMock = jest.fn((...args) => { + resolveHandleSocketMessageMockP(); + handleSocketMessage.apply(socket, args); + }); + (socket as any).handleSocketMessage = handleSocketMessageMock; + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + // Dummy server + socket.setServer(quicServer as unknown as QUICServer); + await socket.start({ + host: '::1', + }); + await ipv6SocketSend( + message, + 0, + message.byteLength, + socket.port, + socket.host, + ); + // Wait for the message to be received + await handleSocketMessageMockP; + // Wait for events to settle + await testsUtils.sleep(0); + expect( + quicServer.acceptConnection.mock.calls.length, + ).toBeLessThanOrEqual(1); + expect(handleEventQUICSocketError).not.toHaveBeenCalled(); + await socket.stop(); + }, ); - socket.connectionMap.delete(connectionId); - await expect(socket.stop()).toResolve(); - }); - test('socket should stop when forced with active connections', async () => { - const socket = new QUICSocket({ - logger, - }); - await socket.start({ - host: '127.0.0.1', - }); - const connectionId = QUICConnectionId.fromBuffer( - Buffer.from('SomeRandomId'), + testProp( + 'new connection failure due to socket errors results in domain error events', + [ + testsUtils.bufferArb({ + minLength: quiche.MAX_CONN_ID_LEN + 1, + maxLength: 100, + }), + ], + async (message) => { + const quicServer = { + acceptConnection: jest + .fn() + .mockRejectedValue(new errors.ErrorQUICSocket()), + }; + // We expect lots of error logs + const socketLogger = logger.getChild('abc'); + socketLogger.setLevel(LogLevel.SILENT); + const socket = new QUICSocket({ + logger: socketLogger, + }); + + // We have to spy on the arrow function property before it is registered + // Which meanst this spy must be created before the socket is started + const { + p: handleSocketMessageMockP, + resolveP: resolveHandleSocketMessageMockP, + } = utils.promise(); + const handleSocketMessage = (socket as any).handleSocketMessage; + const handleSocketMessageMock = jest.fn((...args) => { + resolveHandleSocketMessageMockP(); + handleSocketMessage.apply(socket, args); + }); + (socket as any).handleSocketMessage = handleSocketMessageMock; + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + // Dummy server + socket.setServer(quicServer as unknown as QUICServer); + await socket.start({ + host: '::', + }); + await dualStackSocketSend( + message, + 0, + message.byteLength, + socket.port, + socket.host, + ); + // Wait for the message to be received + await handleSocketMessageMockP; + // Wait for events to settle + await testsUtils.sleep(0); + // Some times the packet is considered as `BufferTooShort` + // So here we branch out depending on whether `acceptConnection` was called + if (quicServer.acceptConnection.mock.calls.length > 0) { + expect(handleEventQUICSocketError).toBeCalledWith( + expect.any(events.EventQUICSocketError), + ); + // The socket is automatically stopped + expect(socket[running]).toBe(false); + } else { + expect(handleEventQUICSocketError).toBeCalledTimes(0); + await socket.stop(); + } + }, + ); + testProp( + 'new connection failure due start timeout is ignored', + [ + testsUtils.bufferArb({ + minLength: quiche.MAX_CONN_ID_LEN + 1, + maxLength: 100, + }), + ], + async (message) => { + const quicServer = { + acceptConnection: jest.fn().mockRejectedValue( + new errors.ErrorQUICServerNewConnection(undefined, { + cause: new errors.ErrorQUICConnectionStartTimeout(), + }), + ), + }; + const socket = new QUICSocket({ + logger, + }); + // We have to spy on the arrow function property before it is registered + // Which meanst this spy must be created before the socket is started + const { + p: handleSocketMessageMockP, + resolveP: resolveHandleSocketMessageMockP, + } = utils.promise(); + const handleSocketMessage = (socket as any).handleSocketMessage; + const handleSocketMessageMock = jest.fn((...args) => { + resolveHandleSocketMessageMockP(); + handleSocketMessage.apply(socket, args); + }); + (socket as any).handleSocketMessage = handleSocketMessageMock; + const handleEventQUICSocketError = jest.fn(); + socket.addEventListener( + events.EventQUICSocketError.name, + handleEventQUICSocketError, + ); + // Dummy server + socket.setServer(quicServer as unknown as QUICServer); + await socket.start({ + host: '::', + }); + await dualStackSocketSend( + message, + 0, + message.byteLength, + socket.port, + socket.host, + ); + // Wait for the message to be received + await handleSocketMessageMockP; + // Wait for events to settle + await testsUtils.sleep(0); + // Some times the packet is considered as `BufferTooShort` + expect( + quicServer.acceptConnection.mock.calls.length, + ).toBeLessThanOrEqual(1); + expect(handleEventQUICSocketError).not.toHaveBeenCalled(); + await socket.stop(); + }, ); - socket.connectionMap.set(connectionId, { - type: 'client', - } as QUICConnection); - await expect(socket.stop({ force: true })).toResolve(); }); }); diff --git a/tests/QUICStream.test.ts b/tests/QUICStream.test.ts index 486f9d6a..1e714c2c 100644 --- a/tests/QUICStream.test.ts +++ b/tests/QUICStream.test.ts @@ -1,14 +1,13 @@ -import type * as events from '@/events'; -import type { ClientCrypto, ServerCrypto } from '@'; -import type QUICSocket from '@/QUICSocket'; +import type { ClientCryptoOps, QUICConnection, ServerCryptoOps } from '@'; import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; import { destroyed } from '@matrixai/async-init'; +import * as events from '@/events'; import * as utils from '@/utils'; import QUICServer from '@/QUICServer'; import QUICClient from '@/QUICClient'; import QUICStream from '@/QUICStream'; import * as testsUtils from './utils'; -import { generateConfig } from './utils'; +import { generateTLSConfig } from './utils'; describe(QUICStream.name, () => { const logger = new Logger(`${QUICStream.name} Test`, LogLevel.WARN, [ @@ -19,34 +18,44 @@ describe(QUICStream.name, () => { const defaultType = 'RSA'; const localhost = '127.0.0.1'; // This has to be setup asynchronously due to key generation - const serverCrypto: ServerCrypto = { + const serverCrypto: ServerCryptoOps = { sign: testsUtils.signHMAC, verify: testsUtils.verifyHMAC, }; let key: ArrayBuffer; - const clientCrypto: ClientCrypto = { + const clientCrypto: ClientCryptoOps = { randomBytes: testsUtils.randomBytes, }; - let sockets: Set; + let socketCleanMethods: ReturnType; + + const testReason = Symbol('TestReason'); + const testCodeToReason = (type, code) => { + switch (code) { + case 2: + return testReason; + default: + return new Error(`${type.toString()} ${code.toString()}`); + } + }; + const testReasonToCode = (type, reason) => { + if (reason === testReason) return 2; + return 1; + }; // We need to test the stream making beforeEach(async () => { key = await testsUtils.generateKeyHMAC(); - sockets = new Set(); + socketCleanMethods = testsUtils.socketCleanupFactory(); }); afterEach(async () => { - const stopProms: Array> = []; - for (const socket of sockets) { - stopProms.push(socket.stop({ force: true })); - } - await Promise.allSettled(stopProms); + await socketCleanMethods.stopSockets(); }); test('should create streams', async () => { const streamsNum = 10; const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -54,15 +63,15 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -79,12 +88,12 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test let streamCount = 0; const streamCreationProm = utils.promise(); - conn.addEventListener('connectionStream', () => { + conn.addEventListener(events.EventQUICConnectionStream.name, () => { streamCount += 1; if (streamCount >= streamsNum) streamCreationProm.resolveP(); }); @@ -92,7 +101,7 @@ describe(QUICStream.name, () => { // const message = Buffer.from('hello!'); const message = Buffer.from('Hello!'); for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); const writer = stream.writable.getWriter(); await writer.write(message); writer.releaseLock(); @@ -106,8 +115,8 @@ describe(QUICStream.name, () => { const message = Buffer.from('Message!'); const streamsNum = 10; const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const streams: Array = []; const server = new QUICServer({ crypto: { @@ -116,15 +125,15 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -141,7 +150,7 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test let streamCreatedCount = 0; @@ -149,8 +158,8 @@ describe(QUICStream.name, () => { const streamCreationProm = utils.promise(); const streamEndedProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { const stream = event.detail; streamCreatedCount += 1; if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); @@ -166,7 +175,7 @@ describe(QUICStream.name, () => { ); // Let's make a new streams. for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); streams.push(stream); const writer = stream.writable.getWriter(); await writer.write(message); @@ -186,8 +195,8 @@ describe(QUICStream.name, () => { const numStreams = 10; const numMessage = 10; const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -195,19 +204,18 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, - port: 58888, }); const client = await QUICClient.createQUICClient({ host: localhost, @@ -221,13 +229,13 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const activeServerStreams: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamProm = stream.readable.pipeTo(stream.writable); activeServerStreams.push(streamProm); @@ -239,7 +247,7 @@ describe(QUICStream.name, () => { for (let i = 0; i < numStreams; i++) { activeClientStreams.push( (async () => { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); const writer = stream.writable.getWriter(); const reader = stream.readable.getReader(); // Do write and read messages here. @@ -263,46 +271,43 @@ describe(QUICStream.name, () => { await server.stop({ force: true }); }); test('should propagate errors over stream for writable', async () => { - const streamsNum = 10; - const testReason = Symbol('TestReason'); - const codeToReason = (type, code) => { - switch (code) { - case 2: - return testReason; - default: - return new Error(`${type.toString()} ${code.toString()}`); - } - }; - const reasonToCode = (type, reason) => { - if (reason === testReason) return 2; - return 1; - }; - const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, ops: serverCrypto, }, logger: logger.getChild(QUICServer.name), - codeToReason, - reasonToCode, + codeToReason: testCodeToReason, + reasonToCode: testReasonToCode, config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); + + const streamProm = utils.promise(); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (evt: events.EventQUICServerConnection) => { + const conn = evt.detail; + conn.addEventListener( + events.EventQUICConnectionStream.name, + (evt: events.EventQUICConnectionStream) => { + streamProm.resolveP(evt.detail); + }, + { once: true }, + ); + }, + { once: true }, ); + await server.start({ host: localhost, - port: 59999, }); + const client = await QUICClient.createQUICClient({ host: localhost, port: server.port, @@ -314,97 +319,70 @@ describe(QUICStream.name, () => { config: { verifyPeer: false, }, - codeToReason, - reasonToCode, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const activeServerStreams: Array> = []; - conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - // Ignore unhandled errors - streamProm.catch(() => {}); - activeServerStreams.push(streamProm); - }, - ); - // Let's make a new streams. - const activeClientStreams: Array> = []; - const message = Buffer.from('Hello!'); - for (let i = 0; i < streamsNum; i++) { - activeClientStreams.push( - (async () => { - const stream = await client.connection.streamNew(); - const writer = stream.writable.getWriter(); - // Do write and read messages here. - await writer.write(message); - await writer.abort(testReason); - try { - for await (const _ of stream.readable) { - // Do nothing, wait for finish - } - } catch (e) { - expect(e).toBe(testReason); - } - })(), - ); - } - const expectationProms = activeServerStreams.map(async (v) => { - await v.catch((e) => { - expect(e).toBe(testReason); - }); + codeToReason: testCodeToReason, + reasonToCode: testReasonToCode, }); - await Promise.all([ - Promise.all(activeClientStreams), - Promise.all(expectationProms), - ]); + socketCleanMethods.extractSocket(client); + + // Create a stream + const clientStream = client.connection.newStream(); + + const clientWriter = clientStream.writable.getWriter(); + const clientReader = clientStream.readable.getReader(); + await clientWriter.write(Buffer.from('hello')); + + const serverStream = await streamProm.p; + const serverWriter = serverStream.writable.getWriter(); + const serverReader = serverStream.readable.getReader(); + await serverReader.read(); + + // Forward write error + await clientWriter.abort(testReason); + await expect(serverReader.read()).rejects.toBe(testReason); + await serverWriter.abort(testReason); + await expect(clientReader.read()).rejects.toBe(testReason); + await client.destroy({ force: true }); await server.stop({ force: true }); }); test('should propagate errors over stream for readable', async () => { - const streamsNum = 1; - const testReason = Symbol('TestReason'); - const codeToReason = (type, code) => { - switch (code) { - case 2: - return testReason; - default: - return new Error(`${type.toString()} ${code.toString()}`); - } - }; - const reasonToCode = (type, reason) => { - if (reason === testReason) return 2; - return 1; - }; - const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, ops: serverCrypto, }, logger: logger.getChild(QUICServer.name), - codeToReason, - reasonToCode, + codeToReason: testCodeToReason, + reasonToCode: testReasonToCode, config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); + + const streamProm = utils.promise(); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (evt: events.EventQUICServerConnection) => { + const conn = evt.detail; + conn.addEventListener( + events.EventQUICConnectionStream.name, + (evt: events.EventQUICConnectionStream) => { + streamProm.resolveP(evt.detail); + }, + { once: true }, + ); + }, + { once: true }, ); + await server.start({ host: localhost, - port: 60000, }); + const client = await QUICClient.createQUICClient({ host: localhost, port: server.port, @@ -416,62 +394,36 @@ describe(QUICStream.name, () => { config: { verifyPeer: false, }, - codeToReason, - reasonToCode, + codeToReason: testCodeToReason, + reasonToCode: testReasonToCode, }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const activeServerStreams: Array> = []; - const serverStreamsProm = utils.promise(); - let serverStreamNum = 0; - conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable - .pipeTo(stream.writable) - .catch((e) => { - expect(e).toBe(testReason); - }); - activeServerStreams.push(streamProm); - serverStreamNum += 1; - if (serverStreamNum >= streamsNum) serverStreamsProm.resolveP(); - }, + socketCleanMethods.extractSocket(client); + + // Create a stream + const clientStream = client.connection.newStream(); + + const clientWriter = clientStream.writable.getWriter(); + const clientReader = clientStream.readable.getReader(); + await clientWriter.write(Buffer.from('hello')); + + const serverStream = await streamProm.p; + const serverWriter = serverStream.writable.getWriter(); + const serverReader = serverStream.readable.getReader(); + await serverReader.read(); + + // Forward write error + await clientReader.cancel(testReason); + await serverReader.cancel(testReason); + // Takes some time for reader cancel to propagate to the writer + await clientStream.closedP; + await serverStream.closedP; + await expect(serverWriter.write(Buffer.from('hello'))).rejects.toBe( + testReason, ); - // Let's make a new streams. - const activeClientStreams: Array> = []; - const message = Buffer.from('Hello!'); - const serverStreamsDoneProm = utils.promise(); - for (let i = 0; i < streamsNum; i++) { - const clientProm = (async () => { - const stream = await client.connection.streamNew(); - const writer = stream.writable.getWriter(); - // Do write and read messages here. - await writer.write(message); - await stream.readable.cancel(testReason); - await serverStreamsDoneProm.p; - // Need time for packets to send/recv - await testsUtils.sleep(100); - const writeProm = writer.write(message); - await writeProm.then( - () => { - throw Error('write did not throw'); - }, - (e) => expect(e).toBe(testReason), - ); - })(); - // ClientProm.catch(e => logger.error(e)); - activeClientStreams.push(clientProm); - } - // Wait for streams to be created before mapping - await serverStreamsProm.p; - await Promise.all([ - Promise.all(activeClientStreams), - Promise.all(activeServerStreams).finally(() => { - serverStreamsDoneProm.resolveP(); - }), - ]); + await expect(clientWriter.write(Buffer.from('hello'))).rejects.toBe( + testReason, + ); + await client.destroy({ force: true }); await server.stop({ force: true }); }); @@ -479,8 +431,8 @@ describe(QUICStream.name, () => { const streamsNum = 10; const message = Buffer.from('The quick brown fox jumped over the lazy dog'); const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -488,15 +440,15 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -513,7 +465,7 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test let streamCreatedCount = 0; @@ -521,8 +473,8 @@ describe(QUICStream.name, () => { const streamCreationProm = utils.promise(); const streamEndedProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (asd: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (asd: events.EventQUICConnectionStream) => { const stream = asd.detail; streamCreatedCount += 1; if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); @@ -538,7 +490,7 @@ describe(QUICStream.name, () => { ); // Let's make a new streams. for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); const writer = stream.writable.getWriter(); await writer.write(message); writer.releaseLock(); @@ -557,8 +509,8 @@ describe(QUICStream.name, () => { const streamsNum = 10; const message = Buffer.from('The quick brown fox jumped over the lazy dog'); const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -566,15 +518,15 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -591,7 +543,7 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test let streamCreatedCount = 0; @@ -599,8 +551,8 @@ describe(QUICStream.name, () => { const streamCreationProm = utils.promise(); const streamEndedProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (asd: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (asd: events.EventQUICConnectionStream) => { const stream = asd.detail; streamCreatedCount += 1; if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); @@ -616,7 +568,7 @@ describe(QUICStream.name, () => { ); // Let's make a new streams. for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); const writer = stream.writable.getWriter(); await writer.write(message); writer.releaseLock(); @@ -634,8 +586,8 @@ describe(QUICStream.name, () => { const streamsNum = 10; const message = Buffer.from('The quick brown fox jumped over the lazy dog'); const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -643,16 +595,16 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, maxIdleTimeout: 100, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), { once: true }, ); await server.start({ @@ -670,7 +622,7 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test let streamCreatedCount = 0; @@ -678,8 +630,8 @@ describe(QUICStream.name, () => { const streamCreationProm = utils.promise(); const streamEndedProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (asd: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (asd: events.EventQUICConnectionStream) => { const stream = asd.detail; streamCreatedCount += 1; if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); @@ -695,7 +647,7 @@ describe(QUICStream.name, () => { ); // Let's make a new streams. for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); + const stream = client.connection.newStream(); const writer = stream.writable.getWriter(); await writer.write(message); writer.releaseLock(); @@ -709,9 +661,9 @@ describe(QUICStream.name, () => { }); test('streams should contain metadata', async () => { const connectionEventProm = - utils.promise(); - const tlsConfig1 = await generateConfig(defaultType); - const tlsConfig2 = await generateConfig(defaultType); + utils.promise(); + const tlsConfig1 = await generateTLSConfig(defaultType); + const tlsConfig2 = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -719,17 +671,17 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, verifyPeer: true, - ca: tlsConfig2.ca, + ca: tlsConfig2.caCertPEM, maxIdleTimeout: 100, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -744,59 +696,59 @@ describe(QUICStream.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, maxIdleTimeout: 100, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { serverStreamProm.resolveP(event.detail); }, ); // Let's make a new streams. const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); + const clientStream = client.connection.newStream(); const writer = clientStream.writable.getWriter(); await writer.write(message); writer.releaseLock(); await serverStreamProm.p; - const clientMetadata = clientStream.remoteInfo; - expect(clientMetadata.localHost).toBe(client.host); - expect(clientMetadata.localPort).toBe(client.port); + const clientMetadata = clientStream.meta; + expect(clientMetadata.localHost).toBe(client.localHost); + expect(clientMetadata.localPort).toBe(client.localPort); expect(clientMetadata.remoteHost).toBe(server.host); expect(clientMetadata.remotePort).toBe(server.port); - expect(clientMetadata.remoteCertificates?.length).toBeGreaterThan(0); - const clientPemChain = utils.certificatePEMsToCertChainPem( - clientMetadata.remoteCertificates!, + expect(clientMetadata.remoteCertsChain?.length).toBeGreaterThan(0); + const clientPemChain = utils.collectPEMs( + clientMetadata.remoteCertsChain.map((v) => utils.derToPEM(v)), ); - expect(clientPemChain).toEqual(tlsConfig1.cert); + expect(clientPemChain[0]).toEqual(tlsConfig1.leafCertPEM); const serverStream = await serverStreamProm.p; - const serverMetadata = serverStream.remoteInfo; + const serverMetadata = serverStream.meta; expect(serverMetadata.localHost).toBe(server.host); expect(serverMetadata.localPort).toBe(server.port); - expect(serverMetadata.remoteHost).toBe(client.host); - expect(serverMetadata.remotePort).toBe(client.port); - expect(serverMetadata.remoteCertificates?.length).toBeGreaterThan(0); - const serverPemChain = utils.certificatePEMsToCertChainPem( - serverMetadata.remoteCertificates!, + expect(serverMetadata.remoteHost).toBe(client.localHost); + expect(serverMetadata.remotePort).toBe(client.localPort); + expect(serverMetadata.remoteCertsChain?.length).toBeGreaterThan(0); + const serverPemChain = utils.collectPEMs( + serverMetadata.remoteCertsChain.map((v) => utils.derToPEM(v)), ); - expect(serverPemChain).toEqual(tlsConfig2.cert); + expect(serverPemChain[0]).toEqual(tlsConfig2.leafCertPEM); await client.destroy({ force: true }); await server.stop({ force: true }); }); test('streams can be cancelled after data sent', async () => { const cancelReason = Symbol('CancelReason'); const connectionEventProm = - utils.promise(); - const tlsConfig1 = await generateConfig(defaultType); - const tlsConfig2 = await generateConfig(defaultType); + utils.promise(); + const tlsConfig1 = await generateTLSConfig(defaultType); + const tlsConfig2 = await generateTLSConfig(defaultType); const reasonConverters = testsUtils.createReasonConverters(); const server = new QUICServer({ crypto: { @@ -805,17 +757,17 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, verifyPeer: true, - ca: tlsConfig2.ca, + ca: tlsConfig2.caCertPEM, }, ...reasonConverters, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -830,24 +782,24 @@ describe(QUICStream.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, }, ...reasonConverters, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { serverStreamProm.resolveP(event.detail); }, ); // Let's make a new streams. const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); + const clientStream = client.connection.newStream(); const writer = clientStream.writable.getWriter(); await writer.write(message); writer.releaseLock(); @@ -880,11 +832,9 @@ describe(QUICStream.name, () => { await server.stop({ force: true }); }); test('streams can be cancelled with no data sent', async () => { - const cancelReason = Symbol('CancelReason'); - const connectionEventProm = - utils.promise(); - const tlsConfig1 = await generateConfig(defaultType); - const tlsConfig2 = await generateConfig(defaultType); + const connectionEventProm = utils.promise(); + const tlsConfig1 = await generateTLSConfig(defaultType); + const tlsConfig2 = await generateTLSConfig(defaultType); const reasonConverters = testsUtils.createReasonConverters(); const server = new QUICServer({ crypto: { @@ -893,17 +843,18 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, verifyPeer: true, - ca: tlsConfig2.ca, + ca: tlsConfig2.caCertPEM, }, ...reasonConverters, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (evt: events.EventQUICServerConnection) => + connectionEventProm.resolveP(evt.detail), ); await server.start({ host: localhost, @@ -918,29 +869,29 @@ describe(QUICStream.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, }, ...reasonConverters, }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; + socketCleanMethods.extractSocket(client); + const conn = await connectionEventProm.p; // Do the test const serverStreamProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { serverStreamProm.resolveP(event.detail); }, ); // Let's make a new streams. - const clientStream = await client.connection.streamNew(); - clientStream.cancel(cancelReason); + const clientStream = client.connection.newStream(); + clientStream.cancel(testReason); await expect(clientStream.readable.getReader().read()).rejects.toBe( - cancelReason, + testReason, ); await expect(clientStream.writable.getWriter().write()).rejects.toBe( - cancelReason, + testReason, ); // Let's check that the server side ended @@ -950,15 +901,16 @@ describe(QUICStream.name, () => { // Just consume until stream throws } })(); - await expect(serverReadProm).rejects.toBe(cancelReason); + await expect(serverReadProm).rejects.toBe(testReason); const serverWriter = serverStream.writable.getWriter(); // Should throw await expect(serverWriter.write(Buffer.from('hello'))).rejects.toBe( - cancelReason, + testReason, ); // And client stream should've cleaned up - await testsUtils.sleep(100); + await clientStream.closedP; + await serverStream.closedP; expect(clientStream[destroyed]).toBeTrue(); await client.destroy({ force: true }); await server.stop({ force: true }); @@ -966,9 +918,9 @@ describe(QUICStream.name, () => { test('streams can be cancelled concurrently after data sent', async () => { const cancelReason = Symbol('CancelReason'); const connectionEventProm = - utils.promise(); - const tlsConfig1 = await generateConfig(defaultType); - const tlsConfig2 = await generateConfig(defaultType); + utils.promise(); + const tlsConfig1 = await generateTLSConfig(defaultType); + const tlsConfig2 = await generateTLSConfig(defaultType); const reasonConverters = testsUtils.createReasonConverters(); const server = new QUICServer({ crypto: { @@ -977,17 +929,17 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, verifyPeer: true, - ca: tlsConfig2.ca, + ca: tlsConfig2.caCertPEM, }, ...reasonConverters, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -1002,24 +954,24 @@ describe(QUICStream.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, }, ...reasonConverters, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const serverStreamProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { serverStreamProm.resolveP(event.detail); }, ); // Let's make a new streams. const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); + const clientStream = client.connection.newStream(); const writer = clientStream.writable.getWriter(); await writer.write(message); writer.releaseLock(); @@ -1051,8 +1003,8 @@ describe(QUICStream.name, () => { // Needed to check that the pull based reading of data doesn't break when we // temporarily run out of data to read const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -1060,15 +1012,15 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -1085,18 +1037,18 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const streamCreationProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { streamCreationProm.resolveP(event.detail); }, ); const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); + const clientStream = client.connection.newStream(); const clientWriter = clientStream.writable.getWriter(); await clientWriter.write(message); await streamCreationProm.p; @@ -1104,6 +1056,7 @@ describe(QUICStream.name, () => { // Drain the readable buffer const serverReader = serverStream.readable.getReader(); + await serverReader.read(); serverReader.releaseLock(); // Closing stream with no buffered data should be responsive @@ -1111,7 +1064,7 @@ describe(QUICStream.name, () => { await serverStream.writable.close(); // Both streams are destroyed even without reading till close - await Promise.all([clientStream.destroyedP, serverStream.destroyedP]); + await Promise.all([clientStream.closedP, serverStream.closedP]); await client.destroy({ force: true }); await server.stop({ force: true }); @@ -1121,8 +1074,8 @@ describe(QUICStream.name, () => { // we will still respond to an error in the readable stream const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); + utils.promise(); + const tlsConfig = await generateTLSConfig(defaultType); const server = new QUICServer({ crypto: { key, @@ -1130,15 +1083,15 @@ describe(QUICStream.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); - testsUtils.extractSocket(server, sockets); + socketCleanMethods.extractSocket(server); server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + events.EventQUICServerConnection.name, + (e: events.EventQUICServerConnection) => connectionEventProm.resolveP(e), ); await server.start({ host: localhost, @@ -1155,18 +1108,18 @@ describe(QUICStream.name, () => { verifyPeer: false, }, }); - testsUtils.extractSocket(client, sockets); + socketCleanMethods.extractSocket(client); const conn = (await connectionEventProm.p).detail; // Do the test const streamCreationProm = utils.promise(); conn.addEventListener( - 'connectionStream', - (event: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (event: events.EventQUICConnectionStream) => { streamCreationProm.resolveP(event.detail); }, ); const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); + const clientStream = client.connection.newStream(); const clientWriter = clientStream.writable.getWriter(); await clientWriter.write(message); await streamCreationProm.p; @@ -1182,11 +1135,11 @@ describe(QUICStream.name, () => { await clientWriter.write(message); // Closing stream with no buffered data should be responsive - await clientWriter.abort(Error('some error')); - await serverWriter.abort(Error('some error')); + await clientWriter.abort(testReason); + await serverWriter.abort(testReason); // Both streams are destroyed even without reading till close - await Promise.all([clientStream.destroyedP, serverStream.destroyedP]); + await Promise.all([clientStream.closedP, serverStream.closedP]); await client.destroy({ force: true }); await server.stop({ force: true }); diff --git a/tests/concurrency.test.ts b/tests/concurrency.test.ts index 0f65f9ba..bd28aa21 100644 --- a/tests/concurrency.test.ts +++ b/tests/concurrency.test.ts @@ -1,14 +1,14 @@ -import type * as events from '@/events'; -import type { ClientCrypto, ServerCrypto, StreamReasonToCode } from '@'; +import type { ClientCryptoOps, ServerCryptoOps, StreamReasonToCode } from '@'; import type { Messages, StreamData } from './utils'; import type { QUICConfig } from '@'; import { fc, testProp } from '@fast-check/jest'; import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; +import * as events from '@/events'; import QUICServer from '@/QUICServer'; import { promise } from '@/utils'; import QUICClient from '@/QUICClient'; import QUICSocket from '@/QUICSocket'; -import { generateConfig, handleStreamProm, sleep } from './utils'; +import { generateTLSConfig, handleStreamProm, sleep } from './utils'; import * as testsUtils from './utils'; describe('Concurrency tests', () => { @@ -19,37 +19,33 @@ describe('Concurrency tests', () => { ]); // This has to be setup asynchronously due to key generation let key: ArrayBuffer; - let clientCrypto: ClientCrypto; - let serverCrypto: ServerCrypto; + let ClientCryptoOps: ClientCryptoOps; + let ServerCryptoOps: ServerCryptoOps; // Tracking resources - let sockets: Array; + let socketCleanMethods: ReturnType; + // Normally we'd bind to a random port, but we need to know it before creating the server for these tests const socketPort1 = 50001; - const reasonToCode = (type: 'recv' | 'send', reason?: any) => { + const reasonToCode = (type: 'read' | 'write', reason?: any) => { logger.error(type, reason); return 0; }; beforeEach(async () => { key = await testsUtils.generateKeyHMAC(); - clientCrypto = { + ClientCryptoOps = { randomBytes: testsUtils.randomBytes, }; - serverCrypto = { + ServerCryptoOps = { sign: testsUtils.signHMAC, verify: testsUtils.verifyHMAC, }; - sockets = []; + socketCleanMethods = testsUtils.socketCleanupFactory(); }); afterEach(async () => { - logger.info('AFTER EACH'); - const stopProms: Array> = []; - for (const socket of sockets) { - stopProms.push(socket.stop({ force: true })); - } - await Promise.allSettled(stopProms); + await socketCleanMethods.stopSockets(); }); const handleClientProm = async ( @@ -60,7 +56,7 @@ describe('Concurrency tests', () => { try { for (const streamData of connectionData.streams) { const streamProm = sleep(streamData.startDelay) - .then(() => client.connection.streamNew()) + .then(() => client.connection.newStream()) .then((stream) => { return handleStreamProm(stream, streamData); }); @@ -122,31 +118,32 @@ describe('Concurrency tests', () => { 'Multiple clients connecting to a server', [connectionsArb, streamsArb(3)], async (clientDatas, serverStreams) => { - const tlsConfig = await generateConfig('RSA'); + const tlsConfig = await generateTLSConfig('RSA'); const cleanUpHoldProm = promise(); const serverProm = (async () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: ServerCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); + socketCleanMethods.extractSocket(server); const connProms: Array> = []; server.addEventListener( - 'serverConnection', - async (e: events.QUICServerConnectionEvent) => { + events.EventQUICServerConnection.name, + async (e: events.EventQUICServerConnection) => { const conn = e.detail; const connProm = (async () => { const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamData = serverStreams[ @@ -200,7 +197,7 @@ describe('Concurrency tests', () => { port: socketPort1, localHost: '::', crypto: { - ops: clientCrypto, + ops: ClientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -210,6 +207,7 @@ describe('Concurrency tests', () => { }); }) .then((client) => { + socketCleanMethods.extractSocket(client); return handleClientProm(client, clientData); }); clientProms.push(clientProm); @@ -244,31 +242,32 @@ describe('Concurrency tests', () => { 'Multiple clients sharing a socket', [connectionsArb, streamsArb(3)], async (clientDatas, serverStreams) => { - const tlsConfig = await generateConfig('RSA'); + const tlsConfig = await generateTLSConfig('RSA'); const cleanUpHoldProm = promise(); const serverProm = (async () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: ServerCryptoOps, }, logger: logger.getChild(QUICServer.name), config: { - key: tlsConfig.key, - cert: tlsConfig.cert, + key: tlsConfig.leafKeyPairPEM.privateKey, + cert: tlsConfig.leafCertPEM, verifyPeer: false, }, }); + socketCleanMethods.extractSocket(server); const connProms: Array> = []; server.addEventListener( - 'serverConnection', - async (e: events.QUICServerConnectionEvent) => { + events.EventQUICServerConnection.name, + async (e: events.EventQUICServerConnection) => { const conn = e.detail; const connProm = (async () => { const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamData = serverStreams[ @@ -329,7 +328,7 @@ describe('Concurrency tests', () => { port: socketPort1, socket, crypto: { - ops: clientCrypto, + ops: ClientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -339,6 +338,7 @@ describe('Concurrency tests', () => { }); }) .then((client) => { + socketCleanMethods.extractSocket(client); return handleClientProm(client, clientData); }); clientProms.push(clientProm); @@ -378,7 +378,7 @@ describe('Concurrency tests', () => { serverStreams, reasonToCode, }: { - socket: QUICSocket | undefined; + socket: QUICSocket; port: number | undefined; cleanUpHoldProm: Promise; config: Partial & { @@ -391,23 +391,24 @@ describe('Concurrency tests', () => { const server = new QUICServer({ crypto: { key, - ops: serverCrypto, + ops: ServerCryptoOps, }, socket, logger: logger.getChild(QUICServer.name), config, reasonToCode, }); + socketCleanMethods.extractSocket(server); const connProms: Array> = []; server.addEventListener( - 'serverConnection', - async (e: events.QUICServerConnectionEvent) => { + events.EventQUICServerConnection.name, + async (e: events.EventQUICServerConnection) => { const conn = e.detail; const connProm = (async () => { const serverStreamProms: Array> = []; conn.addEventListener( - 'connectionStream', - (streamEvent: events.QUICConnectionStreamEvent) => { + events.EventQUICConnectionStream.name, + (streamEvent: events.EventQUICConnectionStream) => { const stream = streamEvent.detail; const streamData = serverStreams[serverStreamProms.length % serverStreams.length]; @@ -453,8 +454,8 @@ describe('Concurrency tests', () => { 'Multiple clients sharing a socket with a server', [connectionsArb, connectionsArb, streamsArb(3), streamsArb(3)], async (clientDatas1, clientDatas2, serverStreams1, serverStreams2) => { - const tlsConfig1 = await generateConfig('RSA'); - const tlsConfig2 = await generateConfig('RSA'); + const tlsConfig1 = await generateTLSConfig('RSA'); + const tlsConfig2 = await generateTLSConfig('RSA'); const clientsInfosA = clientDatas1.map((v) => v.streams.length); const clientsInfosB = clientDatas2.map((v) => v.streams.length); logger.info(`clientsA: ${clientsInfosA}`); @@ -467,8 +468,8 @@ describe('Concurrency tests', () => { const socket2 = new QUICSocket({ logger: logger.getChild('socket'), }); - sockets.push(socket1); - sockets.push(socket2); + socketCleanMethods.sockets.add(socket1); + socketCleanMethods.sockets.add(socket2); await socket1.start({ host: '127.0.0.1', }); @@ -482,8 +483,8 @@ describe('Concurrency tests', () => { serverStreams: serverStreams1, socket: socket1, config: { - key: tlsConfig1.key, - cert: tlsConfig1.cert, + key: tlsConfig1.leafKeyPairPEM.privateKey, + cert: tlsConfig1.leafCertPEM, verifyPeer: false, logKeys: './tmp/key1.log', initialMaxStreamsBidi: 10000, @@ -496,8 +497,8 @@ describe('Concurrency tests', () => { serverStreams: serverStreams2, socket: socket2, config: { - key: tlsConfig2.key, - cert: tlsConfig2.cert, + key: tlsConfig2.leafKeyPairPEM.privateKey, + cert: tlsConfig2.leafCertPEM, verifyPeer: false, logKeys: './tmp/key2.log', initialMaxStreamsBidi: 10000, @@ -518,7 +519,7 @@ describe('Concurrency tests', () => { port: socket2.port, socket: socket1, crypto: { - ops: clientCrypto, + ops: ClientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -528,6 +529,7 @@ describe('Concurrency tests', () => { }); }) .then((client) => { + socketCleanMethods.extractSocket(client); return handleClientProm(client, clientData); }); clientProms1.push(clientProm); @@ -541,7 +543,7 @@ describe('Concurrency tests', () => { port: socket1.port, socket: socket2, crypto: { - ops: clientCrypto, + ops: ClientCryptoOps, }, logger: logger.getChild(QUICClient.name), config: { @@ -552,6 +554,7 @@ describe('Concurrency tests', () => { }); }) .then((client) => { + socketCleanMethods.extractSocket(client); return handleClientProm(client, clientData); }); clientProms2.push(clientProm); diff --git a/tests/native/connection.test.ts b/tests/native/connection.test.ts new file mode 100644 index 00000000..79453136 --- /dev/null +++ b/tests/native/connection.test.ts @@ -0,0 +1,728 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { + QUICConfig, + Host, + Port, + ClientCryptoOps, + ServerCryptoOps, +} from '@/types'; +import type { Config, Connection, SendInfo } from '@/native/types'; +import { quiche } from '@/native'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import * as utils from '@/utils'; +import * as testsUtils from '../utils'; + +describe('native/connection', () => { + let crypto: { + key: ArrayBuffer; + ops: ClientCryptoOps & ServerCryptoOps; + }; + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + }); + describe('connection lifecycle', () => { + describe('connect and close client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + let clientQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('Hello World')); + expect(clientConn.peerError()).toBeNull(); + // According to RFC9000, if the connection is not in a position + // to send the connection close frame, then the local error + // is changed to be a protocol level error with the `ApplicationError` + // code and a cleared reason. + // If this connection was in a position to send the error, then + // we would expect the `isApp` to be `true`. + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: quiche.ConnectionErrorCode.ApplicationError, + reason: new Uint8Array(), + }); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Client connection is closed (this is not true if there is draining) + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('after client close', async () => { + const randomPacketBuffer = new ArrayBuffer(1000); + await testsUtils.randomBytes(randomPacketBuffer); + const randomPacket = new Uint8Array(randomPacketBuffer); + // Random packets are received after the connection is closed + // However they are just dropped automatically + clientConn.recv(randomPacket, { + to: clientHost, + from: serverHost, + }); + // You can receive multiple times without any problems + clientConn.recv(randomPacket, { + to: clientHost, + from: serverHost, + }); + clientConn.recv(randomPacket, { + to: clientHost, + from: serverHost, + }); + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + }); + }); + describe('connection timeouts', () => { + describe('dialing timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let _clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let _serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + _serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing timeout', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [_clientSendLength, _clientSendInfo] = result!; + expect(clientConn.send(clientBuffer)).toBeNull(); + // Exhaust the timeout + await testsUtils.waitForTimeoutNull(clientConn); + // After max idle timeout, you cannot artificially close the connection + expect(clientConn.close(true, 0, Buffer.from('abc'))).toBeNull(); + // Connection has timed out + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Connection is closed + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + // No errors after max idle timeout + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toBeNull(); + }); + }); + describe('initial timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let _serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + expect(serverConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Once an idle max timeout is set, this timeout is no longer null + // Either the client or server or both can set the idle timeout + expect(serverConn.timeout()).not.toBeNull(); + }); + test('client <-initial- server timeout', async () => { + // Server tries sending the initial frame + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [_serverSendLength, _serverSendInfo] = result!; + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(serverConn.isTimedOut()).toBeFalse(); + // Let's assume the initial frame never gets received by the client + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isTimedOut()).toBeTrue(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + // After both are timed out, there's no local error nor peer error + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toBeNull(); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toBeNull(); + }); + }); + describe('handshake timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + expect(serverConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Once an idle max timeout is set, this timeout is no longer null + // Either the client or server or both can set the idle timeout + expect(serverConn.timeout()).not.toBeNull(); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server timeout', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(serverConn.isTimedOut()).toBeFalse(); + // Let's assume the handshake frame never gets received by the client + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isTimedOut()).toBeTrue(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + }); + describe('established timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + expect(serverConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Once an idle max timeout is set, this timeout is no longer null + // Either the client or server or both can set the idle timeout + expect(serverConn.timeout()).not.toBeNull(); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> sever', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server timeout', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(serverConn.isTimedOut()).toBeFalse(); + // Let's assume the handshake frame never gets received by the client + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isTimedOut()).toBeTrue(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + }); + }); + }); +}); diff --git a/tests/native/quiche.connection.lifecycle.test.ts b/tests/native/quiche.connection.lifecycle.test.ts deleted file mode 100644 index 25485eca..00000000 --- a/tests/native/quiche.connection.lifecycle.test.ts +++ /dev/null @@ -1,2142 +0,0 @@ -import type { X509Certificate } from '@peculiar/x509'; -import type { - QUICConfig, - Host, - Port, - ClientCrypto, - ServerCrypto, -} from '@/types'; -import type { Config, Connection, SendInfo } from '@/native/types'; -import { quiche } from '@/native'; -import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; -import QUICConnectionId from '@/QUICConnectionId'; -import * as utils from '@/utils'; -import * as testsUtils from '../utils'; - -describe('quiche connection lifecycle', () => { - let crypto: { - key: ArrayBuffer; - ops: ClientCrypto & ServerCrypto; - }; - let keyPairRSA: { - publicKey: JsonWebKey; - privateKey: JsonWebKey; - }; - let certRSA: X509Certificate; - let keyPairRSAPEM: { - publicKey: string; - privateKey: string; - }; - let certRSAPEM: string; - let keyPairECDSA: { - publicKey: JsonWebKey; - privateKey: JsonWebKey; - }; - let certECDSA: X509Certificate; - let keyPairECDSAPEM: { - publicKey: string; - privateKey: string; - }; - let certECDSAPEM: string; - let keyPairEd25519: { - publicKey: JsonWebKey; - privateKey: JsonWebKey; - }; - let certEd25519: X509Certificate; - let keyPairEd25519PEM: { - publicKey: string; - privateKey: string; - }; - let certEd25519PEM: string; - beforeAll(async () => { - crypto = { - key: await testsUtils.generateKeyHMAC(), - ops: { - sign: testsUtils.signHMAC, - verify: testsUtils.verifyHMAC, - randomBytes: testsUtils.randomBytes, - }, - }; - keyPairRSA = await testsUtils.generateKeyPairRSA(); - certRSA = await testsUtils.generateCertificate({ - certId: '0', - subjectKeyPair: keyPairRSA, - issuerPrivateKey: keyPairRSA.privateKey, - duration: 60 * 60 * 24 * 365 * 10, - }); - keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); - certRSAPEM = testsUtils.certToPEM(certRSA); - keyPairECDSA = await testsUtils.generateKeyPairECDSA(); - certECDSA = await testsUtils.generateCertificate({ - certId: '0', - subjectKeyPair: keyPairECDSA, - issuerPrivateKey: keyPairECDSA.privateKey, - duration: 60 * 60 * 24 * 365 * 10, - }); - keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); - certECDSAPEM = testsUtils.certToPEM(certECDSA); - keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); - certEd25519 = await testsUtils.generateCertificate({ - certId: '0', - subjectKeyPair: keyPairEd25519, - issuerPrivateKey: keyPairEd25519.privateKey, - duration: 60 * 60 * 24 * 365 * 10, - }); - keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); - certEd25519PEM = testsUtils.certToPEM(certEd25519); - }); - describe('connection lifecycle', () => { - describe('connect and close client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - let clientQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client close', async () => { - clientConn.close(true, 0, Buffer.from('Hello World')); - expect(clientConn.peerError()).toBeNull(); - // According to RFC9000, if the connection is not in a position - // to send the connection close frame, then the local error - // is changed to be a protocol level error with the `ApplicationError` - // code and a cleared reason. - // If this connection was in a position to send the error, then - // we would expect the `isApp` to be `true`. - expect(clientConn.localError()).toEqual({ - isApp: false, - errorCode: quiche.ConnectionErrorCode.ApplicationError, - reason: new Uint8Array(), - }); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - // Client connection is closed (this is not true if there is draining) - expect(clientConn.isClosed()).toBeTrue(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('after client close', async () => { - const randomPacketBuffer = new ArrayBuffer(1000); - await testsUtils.randomBytes(randomPacketBuffer); - const randomPacket = new Uint8Array(randomPacketBuffer); - // Random packets are received after the connection is closed - // However they are just dropped automatically - clientConn.recv(randomPacket, { - to: clientHost, - from: serverHost, - }); - // You can receive multiple times without any problems - clientConn.recv(randomPacket, { - to: clientHost, - from: serverHost, - }); - clientConn.recv(randomPacket, { - to: clientHost, - from: serverHost, - }); - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(clientConn.isClosed()).toBeTrue(); - }); - }); - describe('connection timeouts', () => { - describe('dialing timeout', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let _clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let _serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 2000, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - maxIdleTimeout: 2000, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - _serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing timeout', async () => { - [_clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Exahust the timeout - await testsUtils.waitForTimeoutNull(clientConn); - // Connection has timed out - expect(clientConn.isTimedOut()).toBeTrue(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - // Connection is closed - expect(clientConn.isClosed()).toBeTrue(); - expect(clientConn.isDraining()).toBeFalse(); - // No errors during idle timeout - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toBeNull(); - }); - }); - describe('initial timeout', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let _serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 2000, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - maxIdleTimeout: 2000, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - const token = await utils.mintToken( - clientDcid, - clientHost.host, - crypto, - ); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - expect(serverConn.timeout()).toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // Once an idle max timeout is set, this timeout is no longer null - // Either the client or server or both can set the idle timeout - expect(serverConn.timeout()).not.toBeNull(); - }); - test('client <-initial- server timeout', async () => { - // Server tries sending the initial frame - [_serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(serverConn.isTimedOut()).toBeFalse(); - // Let's assume the initial frame never gets received by the client - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isTimedOut()).toBeTrue(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeTrue(); - expect(serverConn.isDraining()).toBeFalse(); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.isTimedOut()).toBeTrue(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeTrue(); - expect(clientConn.isDraining()).toBeFalse(); - }); - }); - describe('handshake timeout', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 2000, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - maxIdleTimeout: 2000, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - const token = await utils.mintToken( - clientDcid, - clientHost.host, - crypto, - ); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - expect(serverConn.timeout()).toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // Once an idle max timeout is set, this timeout is no longer null - // Either the client or server or both can set the idle timeout - expect(serverConn.timeout()).not.toBeNull(); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server timeout', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(serverConn.isTimedOut()).toBeFalse(); - // Let's assume the handshake frame never gets received by the client - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isTimedOut()).toBeTrue(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeTrue(); - expect(serverConn.isDraining()).toBeFalse(); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.isTimedOut()).toBeTrue(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeTrue(); - expect(clientConn.isDraining()).toBeFalse(); - }); - }); - describe('established timeout', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 2000, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - maxIdleTimeout: 2000, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - const token = await utils.mintToken( - clientDcid, - clientHost.host, - crypto, - ); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - expect(serverConn.timeout()).toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // Once an idle max timeout is set, this timeout is no longer null - // Either the client or server or both can set the idle timeout - expect(serverConn.timeout()).not.toBeNull(); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -handshake-> sever', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server timeout', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(serverConn.isTimedOut()).toBeFalse(); - // Let's assume the handshake frame never gets received by the client - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isTimedOut()).toBeTrue(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeTrue(); - expect(serverConn.isDraining()).toBeFalse(); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.isTimedOut()).toBeTrue(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeTrue(); - expect(clientConn.isDraining()).toBeFalse(); - }); - }); - }); - describe('connection between client and server with RSA', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client dialing', async () => { - // Send the initial packet - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - // The initial frame will always be 1200 bytes - expect(clientSendLength).toBe(1200); - expect(clientSendInfo.from).toEqual(clientHost); - expect(clientSendInfo.to).toEqual(serverHost); - // This is the initial delay for the dialing procedure - // Quiche will repeatedly send the initial packet until it is received - // or exhausted the idle timeout, which in this case is 0 (disabled) - expect(typeof clientConn.timeout()!).toBe('number'); - // The initial delay starts at roughly 1 second - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(1000, -3); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - // Repeating send will throw `Done` - // This proves that only 1 send is necessary at the beginning - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Wait out the delay (add 50ms for non-determinism) - await testsUtils.sleep(clientConn.timeout()! + 50); - // Connection has not timed out because idle timeout defaults to infinity - expect(clientConn.isTimedOut()).toBeFalse(); - // The delay is exhausted, and therefore should be 0 - expect(clientConn.timeout()).toBe(0); - // The `onTimeout` must be called to transition state - clientConn.onTimeout(); - // The delay is repeated immediately after `onTimeout` - // It is still 1 second - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(1000, -3); - // Retry the initial packet - const clientBuffer_ = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer_); - expect(clientSendLength).toBe(1200); - expect(clientSendInfo.from).toEqual(clientHost); - expect(clientSendInfo.to).toEqual(serverHost); - // Retried initial frame is not an exact copy - expect(clientBuffer_).not.toEqual(clientBuffer); - // Upon the retry, the delay now doubles - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(2000, -3); - // This dialing process will repeat max idle timeout is exhausted - // Copy sendBuffer_ into sendBuffer - clientBuffer.set(clientBuffer_); - }); - test('client and server negotiation', async () => { - // Process the initial frame - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // It will be an initial packet - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - // The SCID is what was generated above - expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual( - clientScid, - ); - // The DCID is randomly generated by the client - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - expect(clientDcid).not.toEqual(clientScid); - // The token will be empty - expect(clientHeaderInitial.token).toHaveLength(0); - // The version should be 1 - expect(clientHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); - expect(clientHeaderInitial.versions).toBeNull(); - // Version negotiation - // The version is supported, we don't need to change - expect( - quiche.versionIsSupported(clientHeaderInitial.version), - ).toBeTrue(); - // Derives a new SCID by signing the client's generated DCID - // This is only used during the stateless retry - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken( - clientDcid, - clientHost.host, - crypto, - ); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - const timeoutBeforeRecv = clientConn.timeout(); - const serverHeaderRetry = quiche.Header.fromSlice( - retryDatagram.subarray(0, retryDatagramLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderRetry.ty).toBe(quiche.Type.Retry); - // Retry packet's SCID is the derived SCID - expect(new QUICConnectionId(serverHeaderRetry.scid)).toEqual( - serverScid, - ); - expect(new QUICConnectionId(serverHeaderRetry.dcid)).toEqual( - clientScid, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - const timeoutAfterRecv = clientConn.timeout(); - // The timeout is only reset after `recv` is called - expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(clientHeaderInitialRetry.scid)).toEqual( - clientScid, - ); - // The DCID is now updated to the server generated one - expect(new QUICConnectionId(clientHeaderInitialRetry.dcid)).toEqual( - serverScid, - ); - // The retried initial packet has the signed token - expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); - expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); - expect(clientHeaderInitialRetry.versions).toBeNull(); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - // Now that both the client and server has selected their own SCID, where - // the server derived its SCID from the initial client's randomly - // generated DCID, we can update their respective DCID - // This means that the client's connection ID is still the randomly - // generated SCID at the beginning, while the server's connection ID - // is the derived SCID when it sent the retry packet. - clientDcid = serverScid; - serverDcid = clientScid; - // Server receives the retried initial frame - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // The timeout is still null upon the first recv for the server - // This is only true because timeout is `0` which is `Infinity` - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Server's responds with an initial frame - expect(serverSendLength).toBe(1200); - // The server is now setting its timeout to start at 1 second - expect(serverConn.timeout()).toBeCloseTo(1000, -3); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - // At this point the server connection is still not established - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - const serverHeaderInitial = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual( - serverScid, - ); - expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual( - serverDcid, - ); - expect(serverHeaderInitial.token).toHaveLength(0); - expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); - expect(serverHeaderInitial.versions).toBeNull(); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - // Timeout is lowered - expect(clientConn.timeout()).toBeLessThan(100); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderHandshake = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(new QUICConnectionId(serverHeaderHandshake.scid)).toEqual( - serverScid, - ); - expect(new QUICConnectionId(serverHeaderHandshake.dcid)).toEqual( - serverDcid, - ); - // Timeout is lowered - expect(serverConn.timeout()).toBeLessThan(100); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - // Client receives server's handshake frame - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -handshake-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderHandshake = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - // SCID is dropped on the short frame - expect(serverHeaderShort.scid).toHaveLength(0); - expect(new QUICConnectionId(serverHeaderShort.dcid)).toEqual( - clientScid, - ); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - // Client connection timeout is now null - // Both client and server is established - // This is due to max idle timeout of 0 - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - // Timeout is lowered - expect(serverConn.timeout()).toBeLessThan(100); - }); - test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - // SCID is dropped on the short frame - expect(clientHeaderShort.scid).toHaveLength(0); - expect(new QUICConnectionId(clientHeaderShort.dcid)).toEqual( - serverScid, - ); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - }); - test('client close', async () => { - clientConn.close(true, 0, Buffer.from('Application Close')); - expect(clientConn.localError()).toEqual({ - isApp: true, - errorCode: 0, - reason: new Uint8Array(Buffer.from('Application Close')), - }); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - // The timeout begins again - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - // Connection is still established - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Connection however begins draining - expect(clientConn.isDraining()).toBeTrue(); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Client connection now waits to be closed - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - // Timeout is finally null - expect(clientConn.timeout()).toBeNull(); - // Connection did not timeout from idleness - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - // Connection is left as established - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - // Connection is fully closed - expect(clientConn.isClosed()).toBeTrue(); - // Connection is left as draining - expect(clientConn.isDraining()).toBeTrue(); - // -short-> SERVER - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // The server receives the client's error - expect(serverConn.peerError()).toEqual({ - isApp: true, - errorCode: 0, - reason: new Uint8Array(Buffer.from('Application Close')), - }); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // SERVER draining - expect(serverConn.isDraining()).toBeTrue(); - // Once the server is in draining, it does not need to respond - // it just waits to timeout, during that time, it is in "draining" state - // We need to exhaust the server's timeout to be fully closed - // Unlike TCP, there is no half-closed state for QUIC connections - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); - // Connection did not timeout from idleness - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - // Connection is left as established - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - // Connection is fully closed - expect(serverConn.isClosed()).toBeTrue(); - // Connection is left as draining - expect(serverConn.isDraining()).toBeTrue(); - }); - }); - describe('connection between client and server with ECDSA', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client dialing', async () => { - // Send the initial packet - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - // The initial frame will always be 1200 bytes - expect(clientSendLength).toBe(1200); - expect(clientSendInfo.from).toEqual(clientHost); - expect(clientSendInfo.to).toEqual(serverHost); - // This is the initial delay for the dialing procedure - // Quiche will repeatedly send the initial packet until it is received - // or exhausted the idle timeout, which in this case is 0 (disabled) - expect(typeof clientConn.timeout()!).toBe('number'); - // The initial delay starts at roughly 1 second - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(1000, -3); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - // Repeating send will throw `Done` - // This proves that only 1 send is necessary at the beginning - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Wait out the delay (add 50ms for non-determinism) - await testsUtils.sleep(clientConn.timeout()! + 50); - // Connection has not timed out because idle timeout defaults to infinity - expect(clientConn.isTimedOut()).toBeFalse(); - // The delay is exhausted, and therefore should be 0 - expect(clientConn.timeout()).toBe(0); - // The `onTimeout` must be called to transition state - clientConn.onTimeout(); - // The delay is repeated immediately after `onTimeout` - // It is still 1 second - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(1000, -3); - // Retry the initial packet - const clientBuffer_ = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer_); - expect(clientSendLength).toBe(1200); - expect(clientSendInfo.from).toEqual(clientHost); - expect(clientSendInfo.to).toEqual(serverHost); - // Retried initial frame is not an exact copy - expect(clientBuffer_).not.toEqual(clientBuffer); - // Upon the retry, the delay now doubles - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(2000, -3); - // This dialing process will repeat max idle timeout is exhausted - // Copy sendBuffer_ into sendBuffer - clientBuffer.set(clientBuffer_); - }); - test('client and server negotiation', async () => { - // Process the initial frame - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // It will be an initial packet - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - // The SCID is what was generated above - expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual( - clientScid, - ); - // The DCID is randomly generated by the client - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - expect(clientDcid).not.toEqual(clientScid); - // The token will be empty - expect(clientHeaderInitial.token).toHaveLength(0); - // The version should be 1 - expect(clientHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); - expect(clientHeaderInitial.versions).toBeNull(); - // Version negotiation - // The version is supported, we don't need to change - expect( - quiche.versionIsSupported(clientHeaderInitial.version), - ).toBeTrue(); - // Derives a new SCID by signing the client's generated DCID - // This is only used during the stateless retry - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken( - clientDcid, - clientHost.host, - crypto, - ); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - const timeoutBeforeRecv = clientConn.timeout(); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - const timeoutAfterRecv = clientConn.timeout(); - // The timeout is only reset after `recv` is called - expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(clientHeaderInitialRetry.scid)).toEqual( - clientScid, - ); - // The DCID is now updated to the server generated one - expect(new QUICConnectionId(clientHeaderInitialRetry.dcid)).toEqual( - serverScid, - ); - // The retried initial packet has the signed token - expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); - expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); - expect(clientHeaderInitialRetry.versions).toBeNull(); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - // Now that both the client and server has selected their own SCID, where - // the server derived its SCID from the initial client's randomly - // generated DCID, we can update their respective DCID - clientDcid = serverScid; - serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // The timeout is still null upon the first recv for the server - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Server's responds with an initial frame - expect(serverSendLength).toBe(1200); - // The server is now setting its timeout to start at 1 second - expect(serverConn.timeout()).toBeCloseTo(1000, -3); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - // At this point the server connection is still not established - const serverHeaderInitial = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual( - serverScid, - ); - expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual( - serverDcid, - ); - expect(serverHeaderInitial.token).toHaveLength(0); - expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); - expect(serverHeaderInitial.versions).toBeNull(); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - // Timeout is lowered - expect(clientConn.timeout()).toBeLessThan(100); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - // Client connection timeout is now null - // Both client and server is established - // This is due to max idle timeout of 0 - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - // Timeout is lowered - expect(serverConn.timeout()).toBeLessThan(100); - }); - test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - }); - test('client close', async () => { - clientConn.close(false, 2, new Uint8Array()); - expect(clientConn.localError()).toEqual({ - isApp: false, - errorCode: 2, - reason: new Uint8Array(), - }); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - // The timeout begins again - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - // Connection is still established - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Connection however begins draining - expect(clientConn.isDraining()).toBeTrue(); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Client connection now waits to be closed - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - // Timeout is finally null - expect(clientConn.timeout()).toBeNull(); - // Connection did not timeout from idleness - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - // Connection is left as established - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - // Connection is fully closed - expect(clientConn.isClosed()).toBeTrue(); - // Connection is left as draining - expect(clientConn.isDraining()).toBeTrue(); - // -short-> SERVER - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(serverConn.peerError()).toEqual({ - isApp: false, - errorCode: 2, - reason: new Uint8Array(), - }); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // SERVER draining - expect(serverConn.isDraining()).toBeTrue(); - // Once the server is in draining, it does not need to respond - // it just waits to timeout, during that time, it is in "draining" state - // We need to exhaust the server's timeout to be fully closed - // Unlike TCP, there is no half-closed state for QUIC connections - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); - // Connection did not timeout from idleness - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - // Connection is left as established - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - // Connection is fully closed - expect(serverConn.isClosed()).toBeTrue(); - // Connection is left as draining - expect(serverConn.isDraining()).toBeTrue(); - }); - }); - describe('connection between client and server with Ed25519', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly genrate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client dialing', async () => { - // Send the initial packet - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - // The initial frame will always be 1200 bytes - expect(clientSendLength).toBe(1200); - expect(clientSendInfo.from).toEqual(clientHost); - expect(clientSendInfo.to).toEqual(serverHost); - // This is the initial delay for the dialing procedure - // Quiche will repeatedly send the initial packet until it is received - // or exhausted the idle timeout, which in this case is 0 (disabled) - expect(typeof clientConn.timeout()!).toBe('number'); - // The initial delay starts at roughly 1 second - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(1000, -3); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - // Repeating send will throw `Done` - // This proves that only 1 send is necessary at the beginning - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Wait out the delay (add 50ms for non-determinism) - await testsUtils.sleep(clientConn.timeout()! + 50); - // Connection has not timed out because idle timeout defaults to infinity - expect(clientConn.isTimedOut()).toBeFalse(); - // The delay is exhausted, and therefore should be 0 - expect(clientConn.timeout()).toBe(0); - // The `onTimeout` must be called to transition state - clientConn.onTimeout(); - // The delay is repeated immediately after `onTimeout` - // It is still 1 second - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(1000, -3); - // Retry the initial packet - const clientBuffer_ = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer_); - expect(clientSendLength).toBe(1200); - expect(clientSendInfo.from).toEqual(clientHost); - expect(clientSendInfo.to).toEqual(serverHost); - // Retried initial frame is not an exact copy - expect(clientBuffer_).not.toEqual(clientBuffer); - // Upon the retry, the delay now doubles - // Round to the nearest 1000 - expect(clientConn.timeout()).toBeCloseTo(2000, -3); - // This dialing process will repeat max idle timeout is exhausted - // Copy sendBuffer_ into sendBuffer - clientBuffer.set(clientBuffer_); - }); - test('client and server negotiation', async () => { - // Process the initial frame - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // It will be an initial packet - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - // The SCID is what was generated above - expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual( - clientScid, - ); - // The DCID is randomly generated by the client - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - expect(clientDcid).not.toEqual(clientScid); - // The token will be empty - expect(clientHeaderInitial.token).toHaveLength(0); - // The version should be 1 - expect(clientHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); - expect(clientHeaderInitial.versions).toBeNull(); - // Version negotiation - // The version is supported, we don't need to change - expect( - quiche.versionIsSupported(clientHeaderInitial.version), - ).toBeTrue(); - // Derives a new SCID by signing the client's generated DCID - // This is only used during the stateless retry - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken( - clientDcid, - clientHost.host, - crypto, - ); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - const timeoutBeforeRecv = clientConn.timeout(); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - const timeoutAfterRecv = clientConn.timeout(); - // The timeout is only reset after `recv` is called - expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(clientHeaderInitialRetry.scid)).toEqual( - clientScid, - ); - // The DCID is now updated to the server generated one - expect(new QUICConnectionId(clientHeaderInitialRetry.dcid)).toEqual( - serverScid, - ); - // The retried initial packet has the signed token - expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); - expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); - expect(clientHeaderInitialRetry.versions).toBeNull(); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - // Now that both the client and server has selected their own SCID, where - // the server derived its SCID from the initial client's randomly - // generated DCID, we can update their respective DCID - clientDcid = serverScid; - serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - // The timeout is still null upon the first recv for the server - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Server's responds with an initial frame - expect(serverSendLength).toBe(1200); - // The server is now setting its timeout to start at 1 second - expect(serverConn.timeout()).toBeCloseTo(1000, -3); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - // At this point the server connection is still not established - const serverHeaderInitial = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual( - serverScid, - ); - expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual( - serverDcid, - ); - expect(serverHeaderInitial.token).toHaveLength(0); - expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); - expect(serverHeaderInitial.versions).toBeNull(); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - // Timeout is lowered - expect(clientConn.timeout()).toBeLessThan(100); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - // Client connection timeout is now null - // Both client and server is established - // This is due to max idle timeout of 0 - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - // Timeout is lowered - expect(serverConn.timeout()).toBeLessThan(100); - }); - test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - }); - test('client close', async () => { - clientConn.close(false, 1, Buffer.from('')); - expect(clientConn.localError()).toEqual({ - isApp: false, - errorCode: 1, - reason: new Uint8Array(), - }); - expect(clientConn.timeout()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - // The timeout begins again - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - // Connection is still established - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Connection however begins draining - expect(clientConn.isDraining()).toBeTrue(); - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - // Client connection now waits to be closed - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - // Timeout is finally null - expect(clientConn.timeout()).toBeNull(); - // Connection did not timeout from idleness - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - // Connection is left as established - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - // Connection is fully closed - expect(clientConn.isClosed()).toBeTrue(); - // Connection is left as draining - expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(serverConn.peerError()).toEqual({ - isApp: false, - errorCode: 1, - reason: new Uint8Array(), - }); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // SERVER draining - expect(serverConn.isDraining()).toBeTrue(); - // Once the server is in draining, it does not need to respond - // it just waits to timeout, during that time, it is in "draining" state - // We need to exhaust the server's timeout to be fully closed - // Unlike TCP, there is no half-closed state for QUIC connections - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); - // Connection did not timeout from idleness - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - // Connection is left as established - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - // Connection is fully closed - expect(serverConn.isClosed()).toBeTrue(); - // Connection is left as draining - expect(serverConn.isDraining()).toBeTrue(); - }); - }); - }); -}); diff --git a/tests/native/quiche.stream.lifecycle.test.ts b/tests/native/quiche.stream.lifecycle.test.ts deleted file mode 100644 index a1c59d09..00000000 --- a/tests/native/quiche.stream.lifecycle.test.ts +++ /dev/null @@ -1,2950 +0,0 @@ -import type { Connection, StreamIter } from '@/native'; -import type { ClientCrypto, Host, Port, ServerCrypto } from '@'; -import { quiche, Shutdown } from '@/native'; -import QUICConnectionId from '@/QUICConnectionId'; -import { buildQuicheConfig, clientDefault, serverDefault } from '@/config'; -import * as utils from '@/utils'; -import { sleep } from '@/utils'; -import * as testsUtils from '../utils'; - -function sendPacket( - connectionSource: Connection, - connectionDestination: Connection, -) { - const dataBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const [serverSendLength, sendInfo] = connectionSource.send(dataBuffer); - connectionDestination.recv(dataBuffer.subarray(0, serverSendLength), { - to: sendInfo.to, - from: sendInfo.from, - }); -} - -function iterToArray(iter: StreamIter) { - const array: Array = []; - for (const iterElement of iter) { - array.push(iterElement); - } - return array; -} - -/** - * Does all the steps for initiating a stream on both sides. - * Used as a starting point for a bunch of tests. - */ -function setupStreamState( - connectionSource: Connection, - connectionDestination: Connection, - streamId: number, -) { - const message = Buffer.from('Message'); - connectionSource.streamSend(streamId, message, false); - sendPacket(connectionSource, connectionDestination); - sendPacket(connectionDestination, connectionSource); - // Clearing message buffer - const buffer = Buffer.allocUnsafe(1024); - connectionDestination.streamRecv(streamId, buffer); -} - -describe('quiche stream lifecycle', () => { - const localHost = '127.0.0.1' as Host; - const clientHost = { - host: localHost, - port: 55555 as Port, - }; - const serverHost = { - host: localHost, - port: 55556, - }; - - let crypto: { - key: ArrayBuffer; - ops: ClientCrypto & ServerCrypto; - }; - - let clientConn: Connection; - let serverConn: Connection; - - beforeAll(async () => { - crypto = { - key: await testsUtils.generateKeyHMAC(), - ops: { - sign: testsUtils.signHMAC, - verify: testsUtils.verifyHMAC, - randomBytes: testsUtils.randomBytes, - }, - }; - }); - - describe('with RSA certs', () => { - const setupConnectionsRSA = async () => { - const clientConfig = buildQuicheConfig({ - ...clientDefault, - verifyPeer: false, - }); - const tlsConfigServer = await testsUtils.generateConfig('RSA'); - const serverConfig = buildQuicheConfig({ - ...serverDefault, - - key: tlsConfigServer.key, - cert: tlsConfigServer.cert, - logKeys: './tmp/key.log', - }); - - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - const clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientConfig, - ); - - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let [clientSendLength] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - const clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - - // Derives a new SCID by signing the client's generated DCID - // This is only used during the stateless retry - const serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - - // Client will retry the initial packet with the token - [clientSendLength] = clientConn.send(clientBuffer); - - // Server accept - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverConfig, - ); - // Server receives the retried initial frame - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - // Client <-initial- server - sendPacket(serverConn, clientConn); - // Client -initial-> server - sendPacket(clientConn, serverConn); - // Client <-handshake- server - sendPacket(serverConn, clientConn); - // Client -handshake-> server - sendPacket(clientConn, serverConn); - // Client <-short- server - sendPacket(serverConn, clientConn); - // Client -short-> server - sendPacket(clientConn, serverConn); - // Both are established - }; - - describe('stream can be created', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(setupConnectionsRSA); - - test('initializing stream with 0-len message', () => { - clientConn.streamSend(0, new Uint8Array(0), false); - // No data is sent - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).toContain(0); - - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('Server state does not exist yet', () => { - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('first stream message creates server state', async () => { - const message = Buffer.from('Message'); - expect(clientConn.streamSend(0, message, false)).toEqual( - message.byteLength, - ); - - // Packet should be sent - sendPacket(clientConn, serverConn); - - // Server state for stream is created - expect(iterToArray(serverConn.readable())).toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Reading the message - - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - expect(bytes).toEqual(message.byteLength); - expect(fin).toBe(false); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - message.toString(), - ); - - // State is updated after reading - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Ack is returned - sendPacket(serverConn, clientConn); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('reverse data can be sent', () => { - // Server state before sending - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - const serverStreamCapacity = serverConn.streamCapacity(0); - expect(serverStreamCapacity).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Client state before sending - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Sending data - const message = Buffer.from('Message 2'); - serverConn.streamSend(0, message, false); - - // Server state is updated - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeLessThan(serverStreamCapacity); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Packet is sent - sendPacket(serverConn, clientConn); - - // Client state after sending - expect(iterToArray(clientConn.readable())).toContain(0); - expect(iterToArray(clientConn.writable())).toContain(0); - expect(clientConn.isReadable()).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Read message - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toEqual(message.byteLength); - expect(fin).toBe(false); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - message.toString(), - ); - - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - - // Ack returned - sendPacket(clientConn, serverConn); - - // Server state is updated - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - // Capacity has increased again - expect(serverConn.streamCapacity(0)).toEqual(serverStreamCapacity); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream finishes with 0-len fin', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('closing forward stream with 0-len fin frame', async () => { - clientConn.streamSend(0, new Uint8Array(0), true); - expect(iterToArray(clientConn.readable())).not.toContain(0); - // Not in the writable iterator - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // But still technically writable - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(clientConn, serverConn); - - // Client state, no changes - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Further writes throws `FinalSize` - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('FinalSize'); - - // Server state - expect(iterToArray(serverConn.readable())).toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeTrue(); - // Stream is immediately finished due to no buffered data - expect(serverConn.streamFinished(0)).toBeTrue(); - // Still readable due to 0-len message and fin flag - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - // Message is empty but exists due to fin flag - expect(bytes).toEqual(0); - expect(fin).toBe(true); - - expect(serverConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - // Further reads throw `Done` - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Server sends ack back - sendPacket(serverConn, clientConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('closing reverse stream with 0-len fin frame', async () => { - serverConn.streamSend(0, new Uint8Array(0), true); - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(serverConn, clientConn); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Client state - expect(iterToArray(clientConn.readable())).toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeTrue(); - // Is finished - expect(clientConn.streamFinished(0)).toBeTrue(); - // Still readable - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toEqual(0); - expect(fin).toBe(true); - - expect(clientConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream state is now invalid since both streams have fully closed - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - // Server sends ack back - sendPacket(clientConn, serverConn); - }); - test('server state is cleaned up and invalid', async () => { - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client state is cleaned up and invalid', async () => { - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('Done'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream finishes with data fin', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('closing forward stream with data fin frame', async () => { - clientConn.streamSend(0, Buffer.from('message'), true); - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(clientConn, serverConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Server state - expect(iterToArray(serverConn.readable())).toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeTrue(); - // Stream is not finished due to buffered data - expect(serverConn.streamFinished(0)).toBeFalse(); - // Still readable due to buffered data - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - // Message is empty but exists due to fin flag - expect(bytes).toEqual(7); - expect(fin).toBe(true); - expect(streamBuf.subarray(0, bytes).toString()).toEqual('message'); - - expect(serverConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Server sends ack back - sendPacket(serverConn, clientConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('closing reverse stream with data fin frame', async () => { - serverConn.streamSend(0, Buffer.from('message'), true); - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(serverConn, clientConn); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Client state - expect(iterToArray(clientConn.readable())).toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeTrue(); - // Stream is not finished due to buffered data - expect(clientConn.streamFinished(0)).toBeFalse(); - // Still readable due to buffered data - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toEqual(7); - expect(fin).toBe(true); - expect(streamBuf.subarray(0, bytes).toString()).toEqual('message'); - - expect(clientConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream state is now invalid since both streams have fully closed - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - - // Server sends ack back - sendPacket(clientConn, serverConn); - }); - test('server state is cleaned up and invalid', async () => { - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client state is cleaned up and invalid', async () => { - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream finishes with buffered data and data fin', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('sending multiple messages on forward stream', async () => { - clientConn.streamSend(0, Buffer.from('Message1 '), false); - clientConn.streamSend(0, Buffer.from('Message2 '), false); - clientConn.streamSend(0, Buffer.from('Message3 '), false); - - // Only one packet is sent - sendPacket(clientConn, serverConn); - sendPacket(serverConn, clientConn); // Ack - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeFalse(); - }); - test('send multiple messages with a fin frame on forward stream', async () => { - clientConn.streamSend(0, Buffer.from('Message1 '), false); - clientConn.streamSend(0, Buffer.from('Message2 '), false); - clientConn.streamSend(0, Buffer.from('Message3 '), true); - - // Only one packet is sent - sendPacket(clientConn, serverConn); - sendPacket(serverConn, clientConn); // Ack - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeFalse(); - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - expect(bytes).toBe(54); - expect(fin).toBeTrue(); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - 'Message1 Message2 Message3 Message1 Message2 Message3 ', - ); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - }); - test('extra writes and reads are invalid on forward stream', async () => { - expect(() => - clientConn.streamSend(0, Buffer.from('invalid1'), false), - ).toThrow('FinalSize'); - expect(() => - clientConn.streamSend(0, Buffer.from('invalid2'), true), - ).toThrow('FinalSize'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - }); - test('sending multiple messages on reverse stream', async () => { - serverConn.streamSend(0, Buffer.from('Message1 '), false); - serverConn.streamSend(0, Buffer.from('Message2 '), false); - serverConn.streamSend(0, Buffer.from('Message3 '), false); - - // Only one packet is sent - sendPacket(serverConn, clientConn); - sendPacket(clientConn, serverConn); // Ack - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeFalse(); - }); - test('send multiple messages with a fin frame on reverse stream', async () => { - serverConn.streamSend(0, Buffer.from('Message1 '), false); - serverConn.streamSend(0, Buffer.from('Message2 '), false); - serverConn.streamSend(0, Buffer.from('Message3 '), true); - - // Only one packet is sent - sendPacket(serverConn, clientConn); - sendPacket(clientConn, serverConn); // Ack - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeFalse(); - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toBe(54); - expect(fin).toBeTrue(); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - 'Message1 Message2 Message3 Message1 Message2 Message3 ', - ); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - }); - test('server state is cleaned up and invalid', async () => { - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client state is cleaned up and invalid', async () => { - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('Done'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream finishes with buffered data and 0-len fin', () => { - // Notes: - // The isFinished doesn't return true until buffered data is read. - // Reading out buffered data will have fin flag be true. - - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('sending multiple messages on forward stream', async () => { - clientConn.streamSend(0, Buffer.from('Message1 '), false); - clientConn.streamSend(0, Buffer.from('Message2 '), false); - clientConn.streamSend(0, Buffer.from('Message3 '), false); - - // Only one packet is sent - sendPacket(clientConn, serverConn); - sendPacket(serverConn, clientConn); // Ack - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeFalse(); - }); - test('send 0-len fin on forward stream', async () => { - clientConn.streamSend(0, new Uint8Array(0), true); - - // Only one packet is sent - sendPacket(clientConn, serverConn); - sendPacket(serverConn, clientConn); // Ack - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - expect(serverConn.streamReadable(0)).toBeTrue(); - // Finished is still false - expect(serverConn.streamFinished(0)).toBeFalse(); - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - expect(bytes).toBe(27); - expect(fin).toBeTrue(); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - 'Message1 Message2 Message3 ', - ); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - }); - test('extra writes and reads are invalid on forward stream', async () => { - expect(() => - clientConn.streamSend(0, Buffer.from('invalid1'), false), - ).toThrow('FinalSize'); - expect(() => - clientConn.streamSend(0, Buffer.from('invalid2'), true), - ).toThrow('FinalSize'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - }); - test('sending multiple messages on reverse stream', async () => { - serverConn.streamSend(0, Buffer.from('Message1 '), false); - serverConn.streamSend(0, Buffer.from('Message2 '), false); - serverConn.streamSend(0, Buffer.from('Message3 '), false); - - // Only one packet is sent - sendPacket(serverConn, clientConn); - sendPacket(clientConn, serverConn); // Ack - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeFalse(); - }); - test('send 0-len fin on reverse stream', async () => { - serverConn.streamSend(0, new Uint8Array(0), true); - - // Only one packet is sent - sendPacket(serverConn, clientConn); - sendPacket(clientConn, serverConn); // Ack - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - expect(clientConn.streamReadable(0)).toBeTrue(); - // Finished is still false due to buffered data - expect(clientConn.streamFinished(0)).toBeFalse(); - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toBe(27); - expect(fin).toBeTrue(); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - 'Message1 Message2 Message3 ', - ); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - }); - test('server state is cleaned up and invalid', async () => { - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client state is cleaned up and invalid', async () => { - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream finishes with 0-len fin before any data', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - }); - - test('initializing stream with no data', async () => { - clientConn.streamSend(0, new Uint8Array(0), false); - - // Local state exists - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - - // No packets are sent, therefor no remote state created - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - test('closing forward stream with 0-len fin frame', async () => { - clientConn.streamSend(0, new Uint8Array(0), true); - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(clientConn, serverConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Further writes fail - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('FinalSize'); - - // Server state - expect(iterToArray(serverConn.readable())).toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeTrue(); - // Stream is immediately finished due to no buffered data - expect(serverConn.streamFinished(0)).toBeTrue(); - // Still readable due to 0-len message and fin flag - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - // Message is empty but exists due to fin flag - expect(bytes).toEqual(0); - expect(fin).toBe(true); - - expect(serverConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Server sends ack back - sendPacket(serverConn, clientConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('closing reverse stream with 0-len fin frame', async () => { - serverConn.streamSend(0, new Uint8Array(0), true); - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(serverConn, clientConn); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Client state - expect(iterToArray(clientConn.readable())).toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeTrue(); - // Is finished - expect(clientConn.streamFinished(0)).toBeTrue(); - // Still readable - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toEqual(0); - expect(fin).toBe(true); - - expect(clientConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream state is now invalid since both streams have fully closed - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - - // Server sends ack back - sendPacket(clientConn, serverConn); - }); - test('server state is cleaned up and invalid', async () => { - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client state is cleaned up and invalid', async () => { - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream finishes with data fin before any data', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - }); - - test('initializing stream with no data', async () => { - clientConn.streamSend(0, new Uint8Array(0), false); - - // Local state exists - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - - // No packets are sent, therefor no remote state created - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - test('closing forward stream with data fin frame', async () => { - clientConn.streamSend(0, Buffer.from('message'), true); - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(clientConn, serverConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Server state - expect(iterToArray(serverConn.readable())).toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeTrue(); - // Stream not finished due to buffered data - expect(serverConn.streamFinished(0)).toBeFalse(); - // Still readable due to buffered and fin flag - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - // Message is empty but exists due to fin flag - expect(bytes).toEqual(7); - expect(fin).toBe(true); - - expect(serverConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Server sends ack back - sendPacket(serverConn, clientConn); - - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('closing reverse stream with data fin frame', async () => { - serverConn.streamSend(0, Buffer.from('message'), true); - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - sendPacket(serverConn, clientConn); - - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - // Not writable anymore - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - - // Client state - expect(iterToArray(clientConn.readable())).toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeTrue(); - // Stream not finished due to buffered data - expect(clientConn.streamFinished(0)).toBeFalse(); - // Still readable due to buffered and fin flag - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - - // Reading message - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toEqual(7); - expect(fin).toBe(true); - - expect(clientConn.streamFinished(0)).toBeTrue(); - // Nothing left to read - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream state is now invalid since both streams have fully closed - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - - // Server sends ack back - sendPacket(clientConn, serverConn); - }); - test('server state is cleaned up and invalid', async () => { - // Server state - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client state is cleaned up and invalid', async () => { - // Client state - expect(iterToArray(clientConn.readable())).not.toContain(0); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - - // Forcing stream closed tests - describe('stream forced closed by client after initial message', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - describe('closing writable from client', () => { - test('client closes writable', async () => { - // Initial writable states - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).toContain(0); - - // After shutting down - clientConn.streamShutdown(0, Shutdown.Write, 42); - // Further shutdowns throw done - expect(() => - clientConn.streamShutdown(0, Shutdown.Write, 42), - ).toThrow('Done'); - - // States are unchanged - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - // No longer in writable iterator - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('stream is no longer writable on client', async () => { - // Can't write after shutdown - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Still seen as writable - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('server receives packet and updates state', async () => { - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - sendPacket(clientConn, serverConn); - // Stream is both readable and finished - expect(serverConn.isReadable()).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).toContain(0); - }); - test('stream is no longer readable on server', async () => { - // Stream now throws `StreamReset` with code 42 - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - - // Connection is now not readable - expect(serverConn.isReadable()).toBeFalse(); - // Stream is still readable and finished - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - // But not in the iterator - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('client receives response packet and updates state', async () => { - // Initial writable states - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Response is sent - sendPacket(serverConn, clientConn); - - // No changes to stream state on server - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // Client changes - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - }); - test('no further packets sent', async () => { - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('closing readable from client', () => { - test('client closes readable', async () => { - // Initial readable state - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - // After shutting down - clientConn.streamShutdown(0, Shutdown.Read, 42); - // Further shutdowns throw done - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - - // No state change - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('Stream is still readable for client', async () => { - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('server receives packet and updates state', async () => { - // Initial state - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).toContain(0); - - // Sending packet - sendPacket(clientConn, serverConn); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // Stream writable and capacity now throws - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // But still listed as writable - expect(iterToArray(serverConn.writable())).toContain(0); - }); - test('stream no longer writable on server', async () => { - // Writes now throw - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('StreamStopped(42)'); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // No longer listed as writable - expect(iterToArray(serverConn.writable())).not.toContain(0); - }); - test('client receives response packet and updates state', async () => { - // Initial readable states - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - // Response is sent - sendPacket(serverConn, clientConn); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - // No changes to stream state on server - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(serverConn.writable())).not.toContain(0); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream is now finished - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('client stream now finished', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('client responds', async () => { - sendPacket(clientConn, serverConn); // Ack? - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No changes to stream state on server - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(serverConn.writable())).not.toContain(0); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('stream still readable on client', async () => { - // Reading stream will never throw, but it does finish. - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('no more packets sent', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - test('server final stream state', async () => { - // Server states - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - // States change - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client final stream state', async () => { - // Client never reaches invalid state? - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream forced closed by server after initial message', () => { - // This test proves closing is the same from the client side and server side. - // This is expected given the symmetric nature of a quic connection. - - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - describe('closing writable from server', () => { - test('server closes writable', async () => { - // Initial writable states - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).toContain(0); - - // After shutting down - serverConn.streamShutdown(0, Shutdown.Write, 42); - // Further shutdowns throw done - expect(() => - serverConn.streamShutdown(0, Shutdown.Write, 42), - ).toThrow('Done'); - - // States are unchanged - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - // No longer in writable iterator - expect(iterToArray(serverConn.writable())).not.toContain(0); - }); - test('stream is no longer writable on server', async () => { - // Can't write after shutdown - expect(() => - serverConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - expect(() => - serverConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Still seen as writable - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).not.toContain(0); - }); - test('client receives packet and updates state', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - sendPacket(serverConn, clientConn); - // Stream is both readable and finished - expect(clientConn.isReadable()).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).toContain(0); - }); - test('stream is no longer readable on client', async () => { - // Stream now throws `StreamReset` with code 42 - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - - // Connection is now not readable - expect(clientConn.isReadable()).toBeFalse(); - // Stream is still readable and finished - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeTrue(); - // But not in the iterator - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('server receives response packet and updates state', async () => { - // Initial writable states - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(() => - serverConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Response is sent - sendPacket(clientConn, serverConn); - - // No changes to stream state on server - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - // Client changes? - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).not.toContain(0); - expect(() => - serverConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - }); - test('no further packets sent', async () => { - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('closing readable from server', () => { - test('server closes readable', async () => { - // Initial readable state - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // After shutting down - serverConn.streamShutdown(0, Shutdown.Read, 42); - // Further shutdowns throw done - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - - // No state change - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('Stream is still readable for server', async () => { - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('client receives packet and updates state', async () => { - // Initial state - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).toContain(0); - - // Sending packet - sendPacket(serverConn, clientConn); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - // Stream writable and capacity now throws - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // But still listed as writable - expect(iterToArray(clientConn.writable())).toContain(0); - }); - test('stream no longer writable on client', async () => { - // Writes now throw - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('StreamStopped(42)'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // No longer listed as writable - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('server receives response packet and updates state', async () => { - // Initial readable states - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // Response is sent - sendPacket(clientConn, serverConn); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // Client changes - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // No changes to stream state on server - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(clientConn.writable())).not.toContain(0); - - // Server changes - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - // Stream is now finished - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('server stream now finished', async () => { - // Reading still results in done - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('server responds', async () => { - sendPacket(serverConn, clientConn); // Ack? - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - // No changes to stream state on client - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(clientConn.writable())).not.toContain(0); - - // Server changes - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('stream still readable on server', async () => { - // Reading stream will never throw, but it does finish. - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('no more packets sent', async () => { - // No new packets - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - }); - test('client final stream state', async () => { - // Server states - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - // States change - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('Done'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(() => clientConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => clientConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('server final stream state', async () => { - // Client never reaches invalid state? - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream forced closed by client before initial message', () => { - // This tests the case where a stream is initiated on one side but no data is sent. - // So the state is not created on the receiving side before it is closed. - - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - }); - - test('initializing stream with no data', async () => { - clientConn.streamSend(0, new Uint8Array(0), false); - - // Local state exists - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - - // No packets are sent, therefor no remote state created - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - describe('closing writable from client', () => { - test('client closes writable', async () => { - // Initial writable states - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).toContain(0); - - // After shutting down - clientConn.streamShutdown(0, Shutdown.Write, 42); - // Further shutdowns throw done - expect(() => - clientConn.streamShutdown(0, Shutdown.Write, 42), - ).toThrow('Done'); - - // States are unchanged - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - // No longer in writable iterator - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('stream is no longer writable on client', async () => { - // Can't write after shutdown - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Still seen as writable - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('server receives packet and creates state', async () => { - // No local state exists initially - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - - // Packet is sent - sendPacket(clientConn, serverConn); - // State is created - expect(serverConn.isReadable()).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeTrue(); - // And immediately closes - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).toContain(0); - }); - test('stream is no longer readable on server', async () => { - // Stream now throws `StreamReset` with code 42 - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - - // Connection is now not readable - expect(serverConn.isReadable()).toBeFalse(); - // Stream is still readable and finished - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - // But not in the iterator - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('client receives response packet and updates state', async () => { - // Initial writable states - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Response is sent - sendPacket(serverConn, clientConn); - - // No changes to stream state on server - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // Client changes? - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - }); - test('no further packets sent', async () => { - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('closing readable from client', () => { - test('client closes readable', async () => { - // Initial readable state - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - // After shutting down - clientConn.streamShutdown(0, Shutdown.Read, 42); - // Further shutdowns throw done - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - - // No state change - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('Stream is still readable for client', async () => { - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('server receives packet and updates state', async () => { - // Initial state - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).toContain(0); - - // Sending packet - sendPacket(clientConn, serverConn); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // Stream writable and capacity now throws - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // But still listed as writable - expect(iterToArray(serverConn.writable())).toContain(0); - }); - test('stream no longer writable on server', async () => { - // Writes now throw - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('StreamStopped(42)'); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // No longer listed as writable - expect(iterToArray(serverConn.writable())).not.toContain(0); - }); - test('client receives response packet and updates state', async () => { - // Initial readable states - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // Response is sent - sendPacket(serverConn, clientConn); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - // No changes to stream state on server - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(serverConn.writable())).not.toContain(0); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream is now finished - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('client stream now finished', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('client responds', async () => { - sendPacket(clientConn, serverConn); // Ack? - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No changes to stream state on server - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(serverConn.writable())).not.toContain(0); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('stream still readable on client', async () => { - // Reading stream will never throw, but it does finish. - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('no more packets sent', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - test('server final stream state', async () => { - // Server states - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - // States change - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client final stream state', async () => { - // Client never reaches invalid state? - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('stream forced closed by client with buffered data', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('buffering data both ways', async () => { - clientConn.streamSend(0, Buffer.from('Message1'), false); - clientConn.streamSend(0, Buffer.from('Message2'), false); - clientConn.streamSend(0, Buffer.from('Message3'), false); - - serverConn.streamSend(0, Buffer.from('Message1'), false); - serverConn.streamSend(0, Buffer.from('Message2'), false); - serverConn.streamSend(0, Buffer.from('Message3'), false); - - sendPacket(clientConn, serverConn); - sendPacket(serverConn, clientConn); - sendPacket(clientConn, serverConn); - - // No more packets to send - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - describe('closing writable from client', () => { - test('client closes writable', async () => { - // Initial writable states - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).toContain(0); - - // After shutting down - clientConn.streamShutdown(0, Shutdown.Write, 42); - // Further shutdowns throw done - expect(() => - clientConn.streamShutdown(0, Shutdown.Write, 42), - ).toThrow('Done'); - - // States are unchanged - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - // No longer in writable iterator - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('stream is no longer writable on client', async () => { - // Can't write after shutdown - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Still seen as writable - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - }); - test('server receives packet and updates state', async () => { - // Initial state - expect(serverConn.isReadable()).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(serverConn.readable())).toContain(0); - - sendPacket(clientConn, serverConn); - - // Stream is both readable and finished - expect(serverConn.isReadable()).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).toContain(0); - }); - test('stream is no longer readable on server', async () => { - // Stream now throws `StreamReset` with code 42 - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - - // Connection is now not readable - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - }); - test('client receives response packet and updates state', async () => { - // Initial writable states - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - - // Response is sent - sendPacket(serverConn, clientConn); - - // No changes to stream state on server - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(serverConn.readable())).not.toContain(0); - - // Client changes? - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(clientConn.writable())).not.toContain(0); - expect(() => - clientConn.streamSend(0, Buffer.from('hello'), false), - ).toThrow('FinalSize'); - }); - test('no further packets sent', async () => { - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('closing readable from client', () => { - test('client closes readable', async () => { - // Initial readable state - // Readable due to buffered data - expect(clientConn.isReadable()).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).toContain(0); - - // After shutting down - clientConn.streamShutdown(0, Shutdown.Read, 42); - // Further shutdowns throw done - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - - // Client ceases to be readable - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('Stream is still readable for client', async () => { - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('server receives packet and updates state', async () => { - // Initial state - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - expect(iterToArray(serverConn.writable())).toContain(0); - - // Sending packet - sendPacket(clientConn, serverConn); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // Stream writable and capacity now throws - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // But still listed as writable - expect(iterToArray(serverConn.writable())).toContain(0); - }); - test('stream no longer writable on server', async () => { - // Writes now throw - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('StreamStopped(42)'); - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - // No longer listed as writable - expect(iterToArray(serverConn.writable())).not.toContain(0); - }); - test('client receives response packet and updates state', async () => { - // Initial readable states - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - // Response is sent - sendPacket(serverConn, clientConn); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream is now finished - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - - // No changes to stream state on server - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(serverConn.writable())).not.toContain(0); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - // Stream is now finished - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('client stream now finished', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('client responds', async () => { - sendPacket(clientConn, serverConn); // Ack? - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No changes to stream state on server - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'StreamStopped(42)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'StreamStopped(42)', - ); - expect(iterToArray(serverConn.writable())).not.toContain(0); - - // Client changes - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('stream still readable on client', async () => { - // Reading stream will never throw, but it does finish. - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(iterToArray(clientConn.readable())).not.toContain(0); - }); - test('no more packets sent', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - test('server final stream state', async () => { - // Server states - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('StreamStopped(42)'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'StreamReset(42)', - ); - // States change - expect(() => - serverConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('Done'); - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeTrue(); - expect(() => serverConn.streamWritable(0, 0)).toThrow( - 'InvalidStreamState(0)', - ); - expect(() => serverConn.streamCapacity(0)).toThrow( - 'InvalidStreamState(0)', - ); - }); - test('client final stream state', async () => { - // Client never reaches invalid state? - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - expect(() => - clientConn.streamSend(0, Buffer.from('message'), true), - ).toThrow('FinalSize'); - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeTrue(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - - // Connection closing - // Note: - // It seems that stream states are not aware of connection states. - // So a closing stream does not trigger streams ending or even cleaning up. - // This also means, normal stream cleanup expectations don't happen. - // Stream will still be writable but throw. - // Stream will still be readable but never finish. - describe('connection closes with active stream, no buffered stream data', () => { - // Note: - // Seems like stream state is not cleaned up by the stream closing. - // We can still write to it and the capacity will change, so the writable is still being buffered? - // Do we need to close the stream to free up memory? - - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('client closing connection', async () => { - clientConn.close(true, 42, Buffer.from('some reason')); - - sendPacket(clientConn, serverConn); - - expect(clientConn.isDraining()).toBeTrue(); - expect(clientConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeTrue(); - expect(serverConn.isClosed()).toBeFalse(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('client stream still functions', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - - // Can still send - expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('server stream still functions', async () => { - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - - // Can still send - expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - test('waiting for closed state', async () => { - await sleep(100); - await Promise.all([ - sleep((clientConn.timeout() ?? 0) + 1).then(() => - clientConn.onTimeout(), - ), - sleep((serverConn.timeout() ?? 0) + 1).then(() => - serverConn.onTimeout(), - ), - ]); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - - expect(clientConn.isDraining()).toBeTrue(); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isDraining()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - test('client stream still functions', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - - // Can still send - expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - }); - test('server stream still functions', async () => { - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThan(12000); - - // Can still send - expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('manually clean up client stream state', async () => { - clientConn.streamShutdown(0, Shutdown.Read, 42); - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - clientConn.streamShutdown(0, Shutdown.Write, 42); - expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No change - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - - // Can't send - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('FinalSize'); - // Can still recv - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Still no change - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - }); - test('manually clean up server stream state', async () => { - serverConn.streamShutdown(0, Shutdown.Read, 42); - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - serverConn.streamShutdown(0, Shutdown.Write, 42); - expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No change - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); - - // Can't send - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('FinalSize'); - // Can still recv - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Still no change - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - describe('connection closes with active stream, with buffered stream data', () => { - const streamBuf = Buffer.allocUnsafe(1024); - - beforeAll(async () => { - await setupConnectionsRSA(); - setupStreamState(clientConn, serverConn, 0); - }); - - test('buffering data both ways', async () => { - clientConn.streamSend(0, Buffer.from('Message1'), false); - clientConn.streamSend(0, Buffer.from('Message2'), false); - clientConn.streamSend(0, Buffer.from('Message3'), false); - - serverConn.streamSend(0, Buffer.from('Message1'), false); - serverConn.streamSend(0, Buffer.from('Message2'), false); - serverConn.streamSend(0, Buffer.from('Message3'), false); - - sendPacket(clientConn, serverConn); - sendPacket(serverConn, clientConn); - sendPacket(clientConn, serverConn); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('client closing connection', async () => { - clientConn.close(true, 42, Buffer.from('some reason')); - - sendPacket(clientConn, serverConn); - - expect(clientConn.isDraining()).toBeTrue(); - expect(clientConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeTrue(); - expect(serverConn.isClosed()).toBeFalse(); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('client stream still functions', async () => { - expect(clientConn.isReadable()).toBeTrue(); - expect(clientConn.streamReadable(0)).toBeTrue(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBe(12000); - - // Can still send - expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - const [bytes, fin] = clientConn.streamRecv(0, streamBuf); - expect(bytes).toBe(24); - expect(fin).toBeFalse(); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - 'Message1Message2Message3', - ); - - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - }); - test('server stream still functions', async () => { - expect(serverConn.isReadable()).toBeTrue(); - expect(serverConn.streamReadable(0)).toBeTrue(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBe(12000); - - // Can still send - expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - const [bytes, fin] = serverConn.streamRecv(0, streamBuf); - expect(bytes).toBe(24); - expect(fin).toBeFalse(); - expect(streamBuf.subarray(0, bytes).toString()).toEqual( - 'Message1Message2Message3', - ); - - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThan(12000); - }); - test('waiting for closed state', async () => { - await sleep(100); - await Promise.all([ - sleep((clientConn.timeout() ?? 0) + 1).then(() => - clientConn.onTimeout(), - ), - sleep((serverConn.timeout() ?? 0) + 1).then(() => - serverConn.onTimeout(), - ), - ]); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - - expect(clientConn.isDraining()).toBeTrue(); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isDraining()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - }); - test('client stream still functions', async () => { - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - - // Can still send - expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - }); - test('server stream still functions', async () => { - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThan(12000); - - // Can still send - expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); - // Can still recv - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - test('manually clean up client stream state', async () => { - clientConn.streamShutdown(0, Shutdown.Read, 42); - expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - clientConn.streamShutdown(0, Shutdown.Write, 42); - expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No change - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - - // Can't send - expect(() => - clientConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('FinalSize'); - // Can still recv - expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Still no change - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.streamReadable(0)).toBeFalse(); - expect(clientConn.streamFinished(0)).toBeFalse(); - expect(clientConn.streamWritable(0, 0)).toBeTrue(); - expect(clientConn.streamCapacity(0)).toBeLessThan(12000); - }); - test('manually clean up server stream state', async () => { - serverConn.streamShutdown(0, Shutdown.Read, 42); - expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( - 'Done', - ); - serverConn.streamShutdown(0, Shutdown.Write, 42); - expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( - 'Done', - ); - - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - - // No change - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); - - // Can't send - expect(() => - serverConn.streamSend(0, Buffer.from('message'), false), - ).toThrow('FinalSize'); - // Can still recv - expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); - - // Still no change - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.streamReadable(0)).toBeFalse(); - expect(serverConn.streamFinished(0)).toBeFalse(); - expect(serverConn.streamWritable(0, 0)).toBeTrue(); - expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); - }); - test('no new packets', async () => { - // No new packets - expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); - expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); - }); - }); - }); - // TODO - // test closing writable from both sides, optional? - // test closing readable from both sides, optional? -}); diff --git a/tests/native/quiche.test.ts b/tests/native/quiche.test.ts index c8ada965..186f872e 100644 --- a/tests/native/quiche.test.ts +++ b/tests/native/quiche.test.ts @@ -1,15 +1,25 @@ +import { testProp } from '@fast-check/jest'; import { quiche } from '@/native'; import * as testsUtils from '../utils'; -describe('quiche', () => { - test('frame parsing', async () => { - const frame = Buffer.from('hello world'); - expect(() => - quiche.Header.fromSlice(frame, quiche.MAX_CONN_ID_LEN), - ).toThrow('BufferTooShort'); - // `InvalidPacket` is also possible but even random bytes can - // look like a packet, so it's not tested here - }); +describe('native/quiche', () => { + testProp( + 'packet parsing', + [testsUtils.bufferArb({ minLength: 0, maxLength: 100 })], + (packet) => { + // Remember a UDP payload only has 1 QUIC packet + // But 1 QUIC packet can have multiple QUIC frames + try { + // The `quiche.MAX_CONN_ID_LEN` is 20 bytes + // From 21 bytes it is possible to bypass `BufferTooShort` but it is not guaranteed + // However 20 bytes and under is always `BufferTooShort` + quiche.Header.fromSlice(packet, quiche.MAX_CONN_ID_LEN); + } catch (e) { + expect(e.message).toBe('BufferTooShort'); + // InvalidPacket seems very rare, save it as an example if you find one! + } + }, + ); test('version negotiation', async () => { const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); await testsUtils.randomBytes(scidBuffer); diff --git a/tests/native/quiche.tls.test.ts b/tests/native/quiche.tls.test.ts deleted file mode 100644 index cf18de20..00000000 --- a/tests/native/quiche.tls.test.ts +++ /dev/null @@ -1,3110 +0,0 @@ -import type { X509Certificate } from '@peculiar/x509'; -import type { - QUICConfig, - Host, - Port, - ClientCrypto, - ServerCrypto, -} from '@/types'; -import type { Config, Connection, SendInfo } from '@/native/types'; -import { quiche } from '@/native'; -import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; -import QUICConnectionId from '@/QUICConnectionId'; -import * as utils from '@/utils'; -import { sleep } from '@/utils'; -import * as testsUtils from '../utils'; - -describe('quiche tls', () => { - let crypto: { - key: ArrayBuffer; - ops: ClientCrypto & ServerCrypto; - }; - let keyPairRSA: { - publicKey: JsonWebKey; - privateKey: JsonWebKey; - }; - let certRSA: X509Certificate; - let keyPairRSAPEM: { - publicKey: string; - privateKey: string; - }; - let certRSAPEM: string; - let keyPairECDSA: { - publicKey: JsonWebKey; - privateKey: JsonWebKey; - }; - let certECDSA: X509Certificate; - let keyPairECDSAPEM: { - publicKey: string; - privateKey: string; - }; - let certECDSAPEM: string; - let keyPairEd25519: { - publicKey: JsonWebKey; - privateKey: JsonWebKey; - }; - let certEd25519: X509Certificate; - let keyPairEd25519PEM: { - publicKey: string; - privateKey: string; - }; - let certEd25519PEM: string; - beforeAll(async () => { - crypto = { - key: await testsUtils.generateKeyHMAC(), - ops: { - sign: testsUtils.signHMAC, - verify: testsUtils.verifyHMAC, - randomBytes: testsUtils.randomBytes, - }, - }; - keyPairRSA = await testsUtils.generateKeyPairRSA(); - certRSA = await testsUtils.generateCertificate({ - certId: '0', - subjectKeyPair: keyPairRSA, - issuerPrivateKey: keyPairRSA.privateKey, - duration: 60 * 60 * 24 * 365 * 10, - }); - keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); - certRSAPEM = testsUtils.certToPEM(certRSA); - keyPairECDSA = await testsUtils.generateKeyPairECDSA(); - certECDSA = await testsUtils.generateCertificate({ - certId: '0', - subjectKeyPair: keyPairECDSA, - issuerPrivateKey: keyPairECDSA.privateKey, - duration: 60 * 60 * 24 * 365 * 10, - }); - keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); - certECDSAPEM = testsUtils.certToPEM(certECDSA); - keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); - certEd25519 = await testsUtils.generateCertificate({ - certId: '0', - subjectKeyPair: keyPairEd25519, - issuerPrivateKey: keyPairEd25519.privateKey, - duration: 60 * 60 * 24 * 365 * 10, - }); - keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); - certEd25519PEM = testsUtils.certToPEM(certEd25519); - }); - describe('RSA success', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -handshake-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -short-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - }); - test('client close', async () => { - clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.timeout()).toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('RSA fail verifying client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -handshake-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - // Server rejects client handshake - expect(() => - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }), - ).toThrow('TlsFail'); - expect(serverConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(serverConn.peerError()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderHandshake = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('RSA fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Client rejects server handshake - expect(() => - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }), - ).toThrow('TlsFail'); - - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client -handshake-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderHandshake = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('RSA custom fail verifying client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -handshake-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('server close early', async () => { - serverConn.close(false, 304, Buffer.from('Custom TLS failed')); - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - - expect(serverConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - expect(serverConn.peerError()).toBeNull(); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Should now be draining - expect(serverConn.isDraining()).toBeTrue(); - - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Should now be draining - expect(clientConn.isDraining()).toBeTrue(); - }); - test('client ends after timeout', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(clientConn); - await sleep((clientConn.timeout() ?? 0) + 1); - clientConn.onTimeout(); - expect(clientConn.isClosed()).toBeTrue(); - }); - test('server ends after timeout', async () => { - expect(() => serverConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('RSA custom fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairRSAPEM.privateKey, - cert: certRSAPEM, - ca: certRSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -handshake-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client close early', async () => { - clientConn.close(false, 304, Buffer.from('Custom TLS failed')); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Should now be draining - expect(clientConn.isDraining()).toBeTrue(); - - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Should now be draining - expect(serverConn.isDraining()).toBeTrue(); - }); - test('client ends after timeout', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(clientConn); - await sleep((clientConn.timeout() ?? 0) + 1); - clientConn.onTimeout(); - expect(clientConn.isClosed()).toBeTrue(); - }); - test('server ends after timeout', async () => { - expect(() => serverConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('ECDSA success', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -short-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - }); - test('client close', async () => { - clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.timeout()).toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('ECDSA fail verifying client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - expect(() => - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }), - ).toThrow('TlsFail'); - expect(serverConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(serverConn.peerError()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderHandshake = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('ECDSA fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Client rejects server initial - expect(() => - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }), - ).toThrow('TlsFail'); - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(clientConn.peerError()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('ECDSA custom fail verifying client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - - test('server close early', async () => { - serverConn.close(false, 304, Buffer.from('Custom TLS failed')); - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - - expect(serverConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - expect(serverConn.peerError()).toBeNull(); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Should now be draining - expect(serverConn.isDraining()).toBeTrue(); - - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Should now be draining - expect(clientConn.isDraining()).toBeTrue(); - }); - test('client ends after timeout', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(clientConn); - await sleep((clientConn.timeout() ?? 0) + 1); - clientConn.onTimeout(); - expect(clientConn.isClosed()).toBeTrue(); - }); - test('server ends after timeout', async () => { - expect(() => serverConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('ECDSA custom fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - - test('client close early', async () => { - clientConn.close(false, 304, Buffer.from('Custom TLS failed')); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Should now be draining - expect(clientConn.isDraining()).toBeTrue(); - - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Should now be draining - expect(serverConn.isDraining()).toBeTrue(); - }); - test('client ends after timeout', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(clientConn); - await sleep((clientConn.timeout() ?? 0) + 1); - clientConn.onTimeout(); - expect(clientConn.isClosed()).toBeTrue(); - }); - test('server ends after timeout', async () => { - expect(() => serverConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('Ed25519 success', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('server is established', async () => { - expect(serverConn.isEstablished()).toBeTrue(); - }); - test('client <-short- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client -short-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); - }); - test('client close', async () => { - clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); - await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.timeout()).toBeNull(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('Ed25519 fail verifying client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - }); - test('client is established', async () => { - expect(clientConn.isEstablished()).toBeTrue(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - expect(() => - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }), - ).toThrow('TlsFail'); - expect(serverConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(serverConn.peerError()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-handshake- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderHandshake = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('Ed25519 fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Client rejects server initial - expect(() => - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }), - ).toThrow('TlsFail'); - - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array(), - }); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('Ed25519 custom fail verifying client', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Client rejects server initial - expect(() => - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }), - ).not.toThrow('TlsFail'); - - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toBeNull(); - - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('server close early', async () => { - serverConn.close(false, 304, Buffer.from('Custom TLS failed')); - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - - expect(serverConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - expect(serverConn.peerError()).toBeNull(); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Should now be draining - expect(serverConn.isDraining()).toBeTrue(); - - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }); - - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Should now be draining - expect(clientConn.isDraining()).toBeTrue(); - }); - test('client ends after timeout', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(clientConn); - await sleep((clientConn.timeout() ?? 0) + 1); - clientConn.onTimeout(); - expect(clientConn.isClosed()).toBeTrue(); - }); - test('server ends after timeout', async () => { - expect(() => serverConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('Ed25519 custom fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, _clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, _serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let _serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, - maxIdleTimeout: 0, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign(crypto.key, clientDcid), - 0, - quiche.MAX_CONN_ID_LEN, - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram, - ); - // Retry gets sent back to be processed by the client - clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { - to: clientHost, - from: serverHost, - }); - // Client will retry the initial packet with the token - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto, - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig, - ); - clientDcid = serverScid; - _serverDcid = clientScid; - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - }); - test('client <-initial- server', async () => { - [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); - // Client rejects server initial - expect(() => - clientConn.recv(serverBuffer.subarray(0, serverSendLength), { - to: clientHost, - from: serverHost, - }), - ).not.toThrow('TlsFail'); - - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client -initial-> server', async () => { - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN, - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toBeNull(); - - expect(serverConn.timeout()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client close early', async () => { - clientConn.close(false, 304, Buffer.from('Custom TLS failed')); - [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); - - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeTrue(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Should now be draining - expect(clientConn.isDraining()).toBeTrue(); - - serverConn.recv(clientBuffer.subarray(0, clientSendLength), { - to: serverHost, - from: clientHost, - }); - - expect(serverConn.localError()).toBeNull(); - expect(serverConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: expect.any(Uint8Array), - }); - - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeTrue(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Should now be draining - expect(serverConn.isDraining()).toBeTrue(); - }); - test('client ends after timeout', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(clientConn); - await sleep((clientConn.timeout() ?? 0) + 1); - clientConn.onTimeout(); - expect(clientConn.isClosed()).toBeTrue(); - }); - test('server ends after timeout', async () => { - expect(() => serverConn.send(clientBuffer)).toThrow('Done'); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); -}); diff --git a/tests/native/stream.test.ts b/tests/native/stream.test.ts new file mode 100644 index 00000000..8581ecee --- /dev/null +++ b/tests/native/stream.test.ts @@ -0,0 +1,2865 @@ +import type { Connection, StreamIter } from '@/native'; +import type { ClientCryptoOps, Host, Port, ServerCryptoOps } from '@'; +import { quiche, Shutdown } from '@/native'; +import QUICConnectionId from '@/QUICConnectionId'; +import { buildQuicheConfig, clientDefault, serverDefault } from '@/config'; +import * as utils from '@/utils'; +import * as testsUtils from '../utils'; + +describe('native/stream', () => { + const localHost = '127.0.0.1' as Host; + const clientHost = { + host: localHost, + port: 55555 as Port, + }; + const serverHost = { + host: localHost, + port: 55556, + }; + + let crypto: { + key: ArrayBuffer; + ops: ClientCryptoOps & ServerCryptoOps; + }; + + let clientConn: Connection; + let serverConn: Connection; + + function sendPacket( + connectionSource: Connection, + connectionDestination: Connection, + ): null | void { + const dataBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const result = connectionSource.send(dataBuffer); + if (result === null) return null; + const [serverSendLength, sendInfo] = result; + connectionDestination.recv(dataBuffer.subarray(0, serverSendLength), { + to: sendInfo.to, + from: sendInfo.from, + }); + } + + function iterToArray(iter: StreamIter) { + const array: Array = []; + for (const iterElement of iter) { + array.push(iterElement); + } + return array; + } + + /** + * Does all the steps for initiating a stream on both sides. + * Used as a starting point for a bunch of tests. + */ + function setupStreamState( + connectionSource: Connection, + connectionDestination: Connection, + streamId: number, + ) { + const message = Buffer.from('Message'); + connectionSource.streamSend(streamId, message, false); + sendPacket(connectionSource, connectionDestination); + sendPacket(connectionDestination, connectionSource); + // Clearing message buffer + const buffer = Buffer.allocUnsafe(1024); + connectionDestination.streamRecv(streamId, buffer); + } + + const setupConnectionsRSA = async () => { + const clientConfig = buildQuicheConfig({ + ...clientDefault, + verifyPeer: false, + }); + const tlsConfigServer = await testsUtils.generateTLSConfig('RSA'); + const serverConfig = buildQuicheConfig({ + ...serverDefault, + + key: tlsConfigServer.leafKeyPairPEM.privateKey, + cert: tlsConfigServer.leafCertPEM, + }); + + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + const clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientConfig, + ); + + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + let [clientSendLength] = result!; + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + const clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + + // Derives a new SCID by signing the client's generated DCID + // This is only used during the stateless retry + const serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + + // Client will retry the initial packet with the token + result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength] = result!; + + // Server accept + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverConfig, + ); + // Server receives the retried initial frame + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + // Client <-initial- server + sendPacket(serverConn, clientConn); + // Client -initial-> server + sendPacket(clientConn, serverConn); + // Client <-handshake- server + sendPacket(serverConn, clientConn); + // Client -handshake-> server + sendPacket(clientConn, serverConn); + // Client <-short- server + sendPacket(serverConn, clientConn); + // Client -short-> server + sendPacket(clientConn, serverConn); + // Both are established + }; + + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + }); + + describe('stream can be created', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(setupConnectionsRSA); + + test('initializing stream with 0-len message', () => { + clientConn.streamSend(0, new Uint8Array(0), false); + // No data is sent + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).toContain(0); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('Server state does not exist yet', () => { + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('first stream message creates server state', async () => { + const message = Buffer.from('Message'); + expect(clientConn.streamSend(0, message, false)).toEqual( + message.byteLength, + ); + + // Packet should be sent + sendPacket(clientConn, serverConn); + + // Server state for stream is created + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading the message + + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toEqual(message.byteLength); + expect(fin).toBe(false); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + message.toString(), + ); + + // State is updated after reading + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Ack is returned + sendPacket(serverConn, clientConn); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('reverse data can be sent', () => { + // Server state before sending + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + const serverStreamCapacity = serverConn.streamCapacity(0); + expect(serverStreamCapacity).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state before sending + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Sending data + const message = Buffer.from('Message 2'); + serverConn.streamSend(0, message, false); + + // Server state is updated + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeLessThan(serverStreamCapacity); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Packet is sent + sendPacket(serverConn, clientConn); + + // Client state after sending + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Read message + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toEqual(message.byteLength); + expect(fin).toBe(false); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + message.toString(), + ); + + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + + // Ack returned + sendPacket(clientConn, serverConn); + + // Server state is updated + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + // Capacity has increased again + expect(serverConn.streamCapacity(0)).toEqual(serverStreamCapacity); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream finishes with 0-len fin', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('closing forward stream with 0-len fin frame', async () => { + clientConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not in the writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // But still technically writable + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state, no changes + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Further writes throws `FinalSize` + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream is immediately finished due to no buffered data + expect(serverConn.streamFinished(0)).toBeTrue(); + // Still readable due to 0-len message and fin flag + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + + // Message is empty but exists due to fin flag + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + // Further reads throw `Done` + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('closing reverse stream with 0-len fin frame', async () => { + serverConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Is finished + expect(clientConn.streamFinished(0)).toBeTrue(); + // Still readable + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect( + serverConn.streamSend(0, Buffer.from('message'), false), + ).toBeNull(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect( + clientConn.streamSend(0, Buffer.from('message'), false), + ).toBeNull(); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream finishes with data fin', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('closing forward stream with data fin frame', async () => { + clientConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream is not finished due to buffered data + expect(serverConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered data + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + // Message is empty but exists due to fin flag + expect(bytes).toEqual(7); + expect(fin).toBe(true); + expect(streamBuf.subarray(0, bytes).toString()).toEqual('message'); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('closing reverse stream with data fin frame', async () => { + serverConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Stream is not finished due to buffered data + expect(clientConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered data + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toEqual(7); + expect(fin).toBe(true); + expect(streamBuf.subarray(0, bytes).toString()).toEqual('message'); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream finishes with buffered data and data fin', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('sending multiple messages on forward stream', async () => { + clientConn.streamSend(0, Buffer.from('Message1 '), false); + clientConn.streamSend(0, Buffer.from('Message2 '), false); + clientConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + }); + test('send multiple messages with a fin frame on forward stream', async () => { + clientConn.streamSend(0, Buffer.from('Message1 '), false); + clientConn.streamSend(0, Buffer.from('Message2 '), false); + clientConn.streamSend(0, Buffer.from('Message3 '), true); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toBe(54); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 Message1 Message2 Message3 ', + ); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + }); + test('extra writes and reads are invalid on forward stream', async () => { + expect(() => + clientConn.streamSend(0, Buffer.from('invalid1'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('invalid2'), true), + ).toThrow('FinalSize'); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + }); + test('sending multiple messages on reverse stream', async () => { + serverConn.streamSend(0, Buffer.from('Message1 '), false); + serverConn.streamSend(0, Buffer.from('Message2 '), false); + serverConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + }); + test('send multiple messages with a fin frame on reverse stream', async () => { + serverConn.streamSend(0, Buffer.from('Message1 '), false); + serverConn.streamSend(0, Buffer.from('Message2 '), false); + serverConn.streamSend(0, Buffer.from('Message3 '), true); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toBe(54); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 Message1 Message2 Message3 ', + ); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect( + serverConn.streamSend(0, Buffer.from('message'), false), + ).toBeNull(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect( + clientConn.streamSend(0, Buffer.from('message'), false), + ).toBeNull(); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream finishes with buffered data and 0-len fin', () => { + // Notes: + // The isFinished doesn't return true until buffered data is read. + // Reading out buffered data will have fin flag be true. + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('sending multiple messages on forward stream', async () => { + clientConn.streamSend(0, Buffer.from('Message1 '), false); + clientConn.streamSend(0, Buffer.from('Message2 '), false); + clientConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + }); + test('send 0-len fin on forward stream', async () => { + clientConn.streamSend(0, new Uint8Array(0), true); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + expect(serverConn.streamReadable(0)).toBeTrue(); + // Finished is still false + expect(serverConn.streamFinished(0)).toBeFalse(); + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toBe(27); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 ', + ); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + }); + test('extra writes and reads are invalid on forward stream', async () => { + expect(() => + clientConn.streamSend(0, Buffer.from('invalid1'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('invalid2'), true), + ).toThrow('FinalSize'); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + }); + test('sending multiple messages on reverse stream', async () => { + serverConn.streamSend(0, Buffer.from('Message1 '), false); + serverConn.streamSend(0, Buffer.from('Message2 '), false); + serverConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + }); + test('send 0-len fin on reverse stream', async () => { + serverConn.streamSend(0, new Uint8Array(0), true); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + expect(clientConn.streamReadable(0)).toBeTrue(); + // Finished is still false due to buffered data + expect(clientConn.streamFinished(0)).toBeFalse(); + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toBe(27); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 ', + ); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream finishes with 0-len fin before any data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + }); + + test('initializing stream with no data', async () => { + clientConn.streamSend(0, new Uint8Array(0), false); + + // Local state exists + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + + // No packets are sent, therefor no remote state created + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + test('closing forward stream with 0-len fin frame', async () => { + clientConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Further writes fail + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream is immediately finished due to no buffered data + expect(serverConn.streamFinished(0)).toBeTrue(); + // Still readable due to 0-len message and fin flag + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + // Message is empty but exists due to fin flag + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('closing reverse stream with 0-len fin frame', async () => { + serverConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Is finished + expect(clientConn.streamFinished(0)).toBeTrue(); + // Still readable + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream finishes with data fin before any data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + }); + + test('initializing stream with no data', async () => { + clientConn.streamSend(0, new Uint8Array(0), false); + + // Local state exists + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + + // No packets are sent, therefor no remote state created + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + test('closing forward stream with data fin frame', async () => { + clientConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream not finished due to buffered data + expect(serverConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered and fin flag + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + // Message is empty but exists due to fin flag + expect(bytes).toEqual(7); + expect(fin).toBe(true); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('closing reverse stream with data fin frame', async () => { + serverConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Stream not finished due to buffered data + expect(clientConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered and fin flag + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toEqual(7); + expect(fin).toBe(true); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + + // Forcing stream closed tests + describe('stream forced closed by client after initial message', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + describe('closing writable from client', () => { + test('client closes writable', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // States are unchanged + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + // No longer in writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on client', async () => { + // Can't write after shutdown + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + sendPacket(clientConn, serverConn); + // Stream is both readable and finished + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).toContain(0); + }); + test('stream is no longer readable on server', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(serverConn.isReadable()).toBeFalse(); + // Stream is still readable and finished + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + // But not in the iterator + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(serverConn, clientConn); + + // No changes to stream state on server + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Client changes + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('closing readable from client', () => { + test('client closes readable', async () => { + // Initial readable state + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + + // No state change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('Stream is still readable for client', async () => { + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).toContain(0); + + // Sending packet + sendPacket(clientConn, serverConn); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // Stream writable and capacity now throws + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // But still listed as writable + expect(iterToArray(serverConn.writable())).toContain(0); + }); + test('stream no longer writable on server', async () => { + // Writes now throw + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // No longer listed as writable + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial readable states + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(serverConn, clientConn); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client stream now finished', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client responds', async () => { + sendPacket(clientConn, serverConn); // Ack? + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('stream still readable on client', async () => { + // Reading stream will never throw, but it does finish. + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + test('server final stream state', async () => { + // Server states + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(serverConn.streamSend(0, Buffer.from('message'), true)).toBeNull(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client final stream state', async () => { + // Client never reaches invalid state? + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream forced closed by server after initial message', () => { + // This test proves closing is the same from the client side and server side. + // This is expected given the symmetric nature of a quic connection. + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + describe('closing writable from server', () => { + test('server closes writable', async () => { + // Initial writable states + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).toContain(0); + + // After shutting down + serverConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // States are unchanged + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + // No longer in writable iterator + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on server', async () => { + // Can't write after shutdown + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives packet and updates state', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + sendPacket(serverConn, clientConn); + // Stream is both readable and finished + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).toContain(0); + }); + test('stream is no longer readable on client', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(clientConn.isReadable()).toBeFalse(); + // Stream is still readable and finished + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeTrue(); + // But not in the iterator + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('server receives response packet and updates state', async () => { + // Initial writable states + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(clientConn, serverConn); + + // No changes to stream state on server + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // Client changes? + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('closing readable from server', () => { + test('server closes readable', async () => { + // Initial readable state + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // After shutting down + serverConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + + // No state change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('Stream is still readable for server', async () => { + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives packet and updates state', async () => { + // Initial state + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).toContain(0); + + // Sending packet + sendPacket(serverConn, clientConn); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + // Stream writable and capacity now throws + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // But still listed as writable + expect(iterToArray(clientConn.writable())).toContain(0); + }); + test('stream no longer writable on client', async () => { + // Writes now throw + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // No longer listed as writable + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives response packet and updates state', async () => { + // Initial readable states + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(clientConn, serverConn); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // Client changes + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // No changes to stream state on server + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(clientConn.writable())).not.toContain(0); + + // Server changes + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('server stream now finished', async () => { + // Reading still results in done + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('server responds', async () => { + sendPacket(serverConn, clientConn); // Ack? + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + // No changes to stream state on client + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(clientConn.writable())).not.toContain(0); + + // Server changes + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('stream still readable on server', async () => { + // Reading stream will never throw, but it does finish. + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + }); + test('client final stream state', async () => { + // Server states + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(clientConn.streamSend(0, Buffer.from('message'), true)).toBeNull(); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('server final stream state', async () => { + // Client never reaches invalid state? + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream forced closed by client before initial message', () => { + // This tests the case where a stream is initiated on one side but no data is sent. + // So the state is not created on the receiving side before it is closed. + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + }); + + test('initializing stream with no data', async () => { + clientConn.streamSend(0, new Uint8Array(0), false); + + // Local state exists + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + + // No packets are sent, therefor no remote state created + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + describe('closing writable from client', () => { + test('client closes writable', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // States are unchanged + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + // No longer in writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on client', async () => { + // Can't write after shutdown + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives packet and creates state', async () => { + // No local state exists initially + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Packet is sent + sendPacket(clientConn, serverConn); + // State is created + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + // And immediately closes + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).toContain(0); + }); + test('stream is no longer readable on server', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(serverConn.isReadable()).toBeFalse(); + // Stream is still readable and finished + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + // But not in the iterator + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(serverConn, clientConn); + + // No changes to stream state on server + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Client changes? + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('closing readable from client', () => { + test('client closes readable', async () => { + // Initial readable state + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + + // No state change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('Stream is still readable for client', async () => { + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).toContain(0); + + // Sending packet + sendPacket(clientConn, serverConn); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // Stream writable and capacity now throws + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // But still listed as writable + expect(iterToArray(serverConn.writable())).toContain(0); + }); + test('stream no longer writable on server', async () => { + // Writes now throw + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // No longer listed as writable + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial readable states + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(serverConn, clientConn); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client stream now finished', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client responds', async () => { + sendPacket(clientConn, serverConn); // Ack? + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('stream still readable on client', async () => { + // Reading stream will never throw, but it does finish. + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + test('server final stream state', async () => { + // Server states + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(serverConn.streamSend(0, Buffer.from('message'), true)).toBeNull(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client final stream state', async () => { + // Client never reaches invalid state? + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('stream forced closed by client with buffered data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('buffering data both ways', async () => { + clientConn.streamSend(0, Buffer.from('Message1'), false); + clientConn.streamSend(0, Buffer.from('Message2'), false); + clientConn.streamSend(0, Buffer.from('Message3'), false); + + serverConn.streamSend(0, Buffer.from('Message1'), false); + serverConn.streamSend(0, Buffer.from('Message2'), false); + serverConn.streamSend(0, Buffer.from('Message3'), false); + + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); + + // No more packets to send + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + describe('closing writable from client', () => { + test('client closes writable', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // States are unchanged + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + // No longer in writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on client', async () => { + // Can't write after shutdown + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).toContain(0); + + sendPacket(clientConn, serverConn); + + // Stream is both readable and finished + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).toContain(0); + }); + test('stream is no longer readable on server', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(serverConn, clientConn); + + // No changes to stream state on server + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Client changes? + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('closing readable from client', () => { + test('client closes readable', async () => { + // Initial readable state + // Readable due to buffered data + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + + // Client ceases to be readable + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('Stream is still readable for client', async () => { + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + expect(iterToArray(serverConn.writable())).toContain(0); + + // Sending packet + sendPacket(clientConn, serverConn); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // Stream writable and capacity now throws + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // But still listed as writable + expect(iterToArray(serverConn.writable())).toContain(0); + }); + test('stream no longer writable on server', async () => { + // Writes now throw + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + // No longer listed as writable + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial readable states + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(serverConn, clientConn); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client stream now finished', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client responds', async () => { + sendPacket(clientConn, serverConn); // Ack? + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow('StreamStopped(42)'); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('stream still readable on client', async () => { + // Reading stream will never throw, but it does finish. + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + test('server final stream state', async () => { + // Server states + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(serverConn.streamSend(0, Buffer.from('message'), true)).toBeNull(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client final stream state', async () => { + // Client never reaches invalid state? + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + + // Connection closing + // Note: + // It seems that stream states are not aware of connection states. + // So a closing stream does not trigger streams ending or even cleaning up. + // This also means, normal stream cleanup expectations don't happen. + // Stream will still be writable but throw. + // Stream will still be readable but never finish. + describe('connection closes with active stream, no buffered stream data', () => { + // Note: + // Seems like stream state is not cleaned up by the stream closing. + // We can still write to it and the capacity will change, so the writable is still being buffered? + // Do we need to close the stream to free up memory? + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('client closing connection', async () => { + clientConn.close(true, 42, Buffer.from('some reason')); + + sendPacket(clientConn, serverConn); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + test('waiting for closed state', async () => { + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThan(13500); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('manually clean up client stream state', async () => { + clientConn.streamShutdown(0, Shutdown.Read, 42); + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + clientConn.streamShutdown(0, Shutdown.Write, 42); + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + + // Can't send + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + + // Still no change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + }); + test('manually clean up server stream state', async () => { + serverConn.streamShutdown(0, Shutdown.Read, 42); + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + serverConn.streamShutdown(0, Shutdown.Write, 42); + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(13500); + + // Can't send + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + // Still no change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(13500); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); + describe('connection closes with active stream, with buffered stream data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('buffering data both ways', async () => { + clientConn.streamSend(0, Buffer.from('Message1'), false); + clientConn.streamSend(0, Buffer.from('Message2'), false); + clientConn.streamSend(0, Buffer.from('Message3'), false); + + serverConn.streamSend(0, Buffer.from('Message1'), false); + serverConn.streamSend(0, Buffer.from('Message2'), false); + serverConn.streamSend(0, Buffer.from('Message3'), false); + + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('client closing connection', async () => { + clientConn.close(true, 42, Buffer.from('some reason')); + + sendPacket(clientConn, serverConn); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(13500); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + const result = clientConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toBe(24); + expect(fin).toBeFalse(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1Message2Message3', + ); + + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(13500); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + const result = serverConn.streamRecv(0, streamBuf); + expect(result).not.toBeNull(); + const [bytes, fin] = result!; + expect(bytes).toBe(24); + expect(fin).toBeFalse(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1Message2Message3', + ); + + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThan(13500); + }); + test('waiting for closed state', async () => { + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + + expect(sendPacket(clientConn, serverConn)).toBeNull(); + expect(sendPacket(serverConn, clientConn)).toBeNull(); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThan(13500); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + test('manually clean up client stream state', async () => { + clientConn.streamShutdown(0, Shutdown.Read, 42); + expect(clientConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + clientConn.streamShutdown(0, Shutdown.Write, 42); + expect(clientConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + + // Can't send + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(clientConn.streamRecv(0, streamBuf)).toBeNull(); + + // Still no change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(13500); + }); + test('manually clean up server stream state', async () => { + serverConn.streamShutdown(0, Shutdown.Read, 42); + expect(serverConn.streamShutdown(0, Shutdown.Read, 42)).toBeNull(); + serverConn.streamShutdown(0, Shutdown.Write, 42); + expect(serverConn.streamShutdown(0, Shutdown.Write, 42)).toBeNull(); + + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + + // No change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(13500); + + // Can't send + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(serverConn.streamRecv(0, streamBuf)).toBeNull(); + + // Still no change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(13500); + }); + test('no new packets', async () => { + // No new packets + expect(sendPacket(serverConn, clientConn)).toBeNull(); + expect(sendPacket(clientConn, serverConn)).toBeNull(); + }); + }); +}); diff --git a/tests/native/tls/ecdsa.test.ts b/tests/native/tls/ecdsa.test.ts new file mode 100644 index 00000000..79c5239e --- /dev/null +++ b/tests/native/tls/ecdsa.test.ts @@ -0,0 +1,2508 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { + QUICConfig, + Host, + Port, + ClientCryptoOps, + ServerCryptoOps, + TLSVerifyCallback, +} from '@/types'; +import type { Config, Connection, SendInfo } from '@/native/types'; +import { quiche } from '@/native'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import * as utils from '@/utils'; +import { CryptoError } from '@/native/types'; +import * as testsUtils from '../../utils'; + +describe('native/tls/ecdsa', () => { + let crypto: { + key: ArrayBuffer; + ops: ClientCryptoOps & ServerCryptoOps; + }; + let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certECDSA: X509Certificate; + let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; + }; + let certECDSAPEM: string; + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + }); + describe('ECDSA success with both client and server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + // Closing always results in local error + expect(clientConn.localError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(), + }); + expect(clientConn.peerError()).toBeNull(); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientBufferCopy = Buffer.from(clientBuffer); + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.localError()).toBeNull(); + // Receiving a close is always a peer error + expect(serverConn.peerError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(), + }); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + // There is no acknowledgement after receiving close + expect(serverConn.send(serverBuffer)).toBeNull(); + // Quiche has not implemented a stateless reset + serverConn.recv(clientBufferCopy, { + to: serverHost, + from: clientHost, + }); + expect(serverConn.send(serverBuffer)).toBeNull(); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA success with only server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: false, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + // The client does not supply a certificate, it is expected to be null + // This means there's no chance of having an empty array + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying client with bad client certificate (TlsFail CryptoError.BadCertificate)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.UnknownCA', async () => { + // CryptoError.BadCertificate means the client supplied certificates that failed verification + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying client with no client certificate (TlsFail 372)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail 372', async () => { + // 372 means the client did not supply any certificates + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: 372, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail 372', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: 372, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying server bad server certificate (TlsFail CryptoError.BadCertificate)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // Client rejects server initial + expect(() => + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), + ).toThrow('TlsFail'); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client has local error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.localError()).toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('server has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail with no server certificates (InternalError 1)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error 1', async () => { + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array(), + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderInitial = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error 1', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA with custom verify callback', () => { + describe('ECDSA success with both client and server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback: TLSVerifyCallback = async ( + certs: Array, + _ca, + ) => { + expect(certs).toHaveLength(1); + return undefined; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + await verifyCallback( + clientPeerCertChain, + clientConfig.ca as Array, + ); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + await verifyCallback( + serverPeerCertChain, + serverConfig.ca as Array, + ); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA success with only server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return undefined; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: false, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + await verifyCallback( + clientPeerCertChain, + clientConfig.ca as Array, + ); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + // The client does not supply a certificate, it is expected to be null + // This means there's no chance of having an empty array + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying client with bad client certificate (TlsFail CryptoError.BadCertificate)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Server will accept the client's bad certificate due to the verify callback + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Because the custom verify callback overrides the default verification function + // The server connection is considered established + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + // We can imagine that our verify callback fails on the bad certificate + await expect( + verifyCallback( + serverPeerCertChain, + serverConfig.ca as Array, + ), + ).resolves.toBe(CryptoError.BadCertificate); + // Simulate a CryptoError.BadCertificate as it means the client supplied a bad certificate + serverConn.close(false, CryptoError.BadCertificate, Buffer.from('')); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail BadCertificate', async () => { + // CryptoError.BadCertificate means the client supplied certificates that failed verification + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail BadCertificate', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying client with no client certificate (TlsFail 372)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(0); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Even with the custom verify callback, requiring the certificates + // will make the `recv` fail with `TlsFail` + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + // No certificates is available + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + // There's no need to do this, but for symmetry + await expect( + verifyCallback( + serverPeerCertChain ?? [], + serverConfig.ca as Array, + ), + ).resolves.toBe(CryptoError.BadCertificate); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail 372', async () => { + // 372 means the client did not supply any certificates + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: 372, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail 372', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: 372, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying server bad server certificate (TlsFail CryptoError.BadCertificate)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // Client will accept the server's bad certificate due to the verify callback + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + // Because the custom verify callback overrides the default verification function + // The client connection is considered established + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + // We can imagine that our verify callback fails on the bad certificate + await expect( + verifyCallback( + clientPeerCertChain, + serverConfig.ca as Array, + ), + ).resolves.toBe(CryptoError.BadCertificate); + // Due to an upstream bug, if we were to simulate a close with CryptoError.BadCertificate code + // it would actually break the server connection, the client connection + // would successfully drain and then close, but the server connection is + // left to idle until it times out. + // Therefore instead of closing immediately here, we have to complete the + // handshake by sending a handshake frame to the server, and then + // simulate a close with CryptoError.BadCertificate as the code + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Simulate a TLS failure due to bad certificate + clientConn.close(false, CryptoError.BadCertificate, Buffer.from('')); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client has local error TlsFail CryptoError.BadCertificate', async () => { + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('server has peer error TlsFail CryptoError.BadCertificate', async () => { + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + }); +}); diff --git a/tests/native/tls/ed25519.test.ts b/tests/native/tls/ed25519.test.ts new file mode 100644 index 00000000..925c5109 --- /dev/null +++ b/tests/native/tls/ed25519.test.ts @@ -0,0 +1,2489 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { + QUICConfig, + Host, + Port, + ClientCryptoOps, + ServerCryptoOps, +} from '@/types'; +import type { Config, Connection, SendInfo } from '@/native/types'; +import { quiche } from '@/native'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import * as utils from '@/utils'; +import { CryptoError } from '@/native/types'; +import * as testsUtils from '../../utils'; + +describe('native/tls/ed25519', () => { + let crypto: { + key: ArrayBuffer; + ops: ClientCryptoOps & ServerCryptoOps; + }; + let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certEd25519: X509Certificate; + let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; + }; + let certEd25519PEM: string; + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + }); + describe('Ed25519 success with both client and server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + // Closing always results in local error + expect(clientConn.localError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(), + }); + expect(clientConn.peerError()).toBeNull(); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientBufferCopy = Buffer.from(clientBuffer); + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.localError()).toBeNull(); + // Receiving a close is always a peer error + expect(serverConn.peerError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(), + }); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + // There is no acknowledgement after receiving close + expect(serverConn.send(serverBuffer)).toBeNull(); + // Quiche has not implemented a stateless reset + serverConn.recv(clientBufferCopy, { + to: serverHost, + from: clientHost, + }); + expect(serverConn.send(serverBuffer)).toBeNull(); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 success with only server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: false, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + // The client does not supply a certificate, it is expected to be null + // This means there's no chance of having an empty array + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying client with bad client certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.UnknownCA', async () => { + // CryptoError.UnknownCA means the client supplied certificates that failed verification + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying client with no client certificate (TlsFail CryptoError.CertificateRequired)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.CertificateRequired', async () => { + // CryptoError.CertificateRequired means the client did not supply any certificates + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.CertificateRequired, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.CertificateRequired', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.CertificateRequired, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying server bad server certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // Client rejects server initial + expect(() => + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), + ).toThrow('TlsFail'); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client has local error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.localError()).toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('server has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail with no server certificates (InternalError 1)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error 1', async () => { + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array(), + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderInitial = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error 1', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 with custom verify callback', () => { + describe('Ed25519 success with both client and server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return undefined; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + await verifyCallback(clientPeerCertChain, clientConfig.ca); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + await verifyCallback( + serverPeerCertChain, + utils.collectPEMs(serverConfig.ca), + ); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 success with only server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return undefined; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: false, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + await verifyCallback(clientPeerCertChain, clientConfig.ca); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + // The client does not supply a certificate, it is expected to be null + // This means there's no chance of having an empty array + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying client with bad client certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Server will accept the client's bad certificate due to the verify callback + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Because the custom verify callback overrides the default verification function + // The server connection is considered established + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + // We can imagine that our verify callback fails on the bad certificate + await expect( + verifyCallback(serverPeerCertChain, serverConfig.ca), + ).resolves.toBe(CryptoError.BadCertificate); + // Simulate a CryptoError.UnknownCA as it means the client supplied a bad certificate + serverConn.close(false, CryptoError.UnknownCA, Buffer.from('')); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.UnknownCA', async () => { + // CryptoError.UnknownCA means the client supplied certificates that failed verification + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying client with no client certificate (TlsFail 372)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(0); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Even with the custom verify callback, requiring the certificates + // will make the `recv` fail with `TlsFail` + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + // No certificates is available + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + // There's no need to do this, but for symmetry + await expect( + verifyCallback(serverPeerCertChain ?? [], serverConfig.ca), + ).resolves.toBe(CryptoError.BadCertificate); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail 372', async () => { + // 372 means the client did not supply any certificates + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: 372, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail 372', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: 372, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying server bad server certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // Client will accept the server's bad certificate due to the verify callback + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + // Because the custom verify callback overrides the default verification function + // The client connection is considered established + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + // We can imagine that our verify callback fails on the bad certificate + await expect( + verifyCallback(clientPeerCertChain, serverConfig.ca), + ).resolves.toBe(CryptoError.BadCertificate); + // Due to an upstream bug, if we were to simulate a close with CryptoError.UnknownCA code + // it would actually break the server connection, the client connection + // would successfully drain and then close, but the server connection is + // left to idle until it times out. + // Therefore instead of closing immediately here, we have to complete the + // handshake by sending a handshake frame to the server, and then + // simulate a close with CryptoError.UnknownCA as the code + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Simulate a CryptoError.UnknownCA as it means the client supplied a bad certificate + clientConn.close(false, CryptoError.UnknownCA, Buffer.from('')); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client has local error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('server has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + }); +}); diff --git a/tests/native/tls/rsa.test.ts b/tests/native/tls/rsa.test.ts new file mode 100644 index 00000000..6279108f --- /dev/null +++ b/tests/native/tls/rsa.test.ts @@ -0,0 +1,2733 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { + QUICConfig, + Host, + Port, + ClientCryptoOps, + ServerCryptoOps, +} from '@/types'; +import type { Config, Connection, SendInfo } from '@/native/types'; +import { quiche } from '@/native'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import * as utils from '@/utils'; +import { CryptoError } from '@/native/types'; +import * as testsUtils from '../../utils'; + +describe('native/tls/rsa', () => { + let crypto: { + key: ArrayBuffer; + ops: ClientCryptoOps & ServerCryptoOps; + }; + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + }); + describe('RSA success with both client and server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + expect(clientConn.timeout()).toBeNull(); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // After the first send from the client, the `clientConn` will have a timeout + expect(clientConn.timeout()).not.toBeNull(); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + expect(serverConn.timeout()).toBeNull(); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.timeout()).toBeNull(); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // After the first send from the server, the `serverConn` will have a timeout + expect(serverConn.timeout()).not.toBeNull(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client -handshake-> server 2', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + // Closing always results in local error + expect(clientConn.localError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(), + }); + expect(clientConn.peerError()).toBeNull(); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientBufferCopy = Buffer.from(clientBuffer); + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.localError()).toBeNull(); + // Receiving a close is always a peer error + expect(serverConn.peerError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(), + }); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + // There is no acknowledgement after receiving close + expect(serverConn.send(serverBuffer)).toBeNull(); + // Quiche has not implemented a stateless reset + serverConn.recv(clientBufferCopy, { + to: serverHost, + from: clientHost, + }); + expect(serverConn.send(serverBuffer)).toBeNull(); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA success with only server certificates', () => { + // These tests run in-order, and each step is a tate transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: false, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + // The client does not supply a certificate, it is expected to be null + // This means there's no chance of having an empty array + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying client with bad client certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client <-initial- server 2', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Server rejects client handshake + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.UnknownCA', async () => { + // CryptoError.UnknownCA means the client supplied certificates that failed verification + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying client with no client certificate (TlsFail CryptoError.CertificateRequired)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Server rejects client handshake + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.CertificateRequired', async () => { + // CryptoError.CertificateRequired means the client did not supply any certificates + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.CertificateRequired, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.CertificateRequired', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.CertificateRequired, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying server with bad server certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // Client rejects server handshake + expect(() => + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), + ).toThrow('TlsFail'); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client has local error TlsFail CryptoError.UnknownCA', async () => { + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderHandshake = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.localError()).toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('server has peer error TlsFail CryptoError.UnknownCA', async () => { + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.UnknownCA, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail with no server certificates (InternalError 1)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error 1', async () => { + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array(), + }); + }); + test('client <-initial- server', () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderInitial = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error 1', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA with custom verify callback', () => { + describe('RSA success with both client and server certificates', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return undefined; + }; + beforeAll(() => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client <-initial- server 2', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client -initial-> server 2', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + await verifyCallback(clientPeerCertChain, clientConfig.ca); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + await verifyCallback(serverPeerCertChain, serverConfig.ca); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA success with only server certificates', () => { + // These tests run in-order, and each step is a tate transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return undefined; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: false, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + await verifyCallback(clientPeerCertChain, clientConfig.ca); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + // The client does not supply a certificate, it is expected to be null + // This means there's no chance of having an empty array + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying client with bad client certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client <-initial- server 2', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client -initial-> server 2', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Server will accept the client's bad certificate due to the verify callback + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Because the custom verify callback overrides the default verification function + // The server connection is considered established + expect(serverConn.isEstablished()).toBeTrue(); + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).not.toBeNull(); + expect(serverPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(serverPeerCertChain[0])).toBe('string'); + // We can imagine that our verify callback fails on the bad certificate + await expect( + verifyCallback(serverPeerCertChain, serverConfig.ca), + ).resolves.toBe(CryptoError.BadCertificate); + // Simulate a CryptoError.BadCertificate as it means the client supplied a bad certificate + serverConn.close(false, CryptoError.BadCertificate, Buffer.from('')); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.BadCertificate', async () => { + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client <-short- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.BadCertificate', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying client with no client certificate (TlsFail CryptoError.CertificateRequired)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(0); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + // Even with the custom verify callback, requiring the certificates + // will make the `recv` fail with `TlsFail` + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + // No certificates is available + const serverPeerCertChain = serverConn.peerCertChain()!; + expect(serverPeerCertChain).toBeNull(); + // There's no need to do this, but for symmetry + await expect( + verifyCallback(serverPeerCertChain ?? [], serverConfig.ca), + ).resolves.toBe(CryptoError.BadCertificate); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server has local error TlsFail CryptoError.CertificateRequired', async () => { + // CryptoError.CertificateRequired means the client did not supply any certificates + expect(serverConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.CertificateRequired, + reason: new Uint8Array(), + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client has peer error TlsFail CryptoError.CertificateRequired', async () => { + expect(clientConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.CertificateRequired, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying server with bad server certificate (TlsFail CryptoError.UnknownCA)', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, _clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, _serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientConfig: QUICConfig; + let serverConfig: QUICConfig; + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let _serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + const verifyCallback = async (certs: Array, _ca) => { + expect(certs).toHaveLength(1); + return CryptoError.BadCertificate; + }; + beforeAll(async () => { + clientConfig = { + ...clientDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + serverConfig = { + ...serverDefault, + verifyPeer: true, + verifyCallback, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + maxIdleTimeout: 0, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + _serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client <-initial- server 2', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + const result = serverConn.send(serverBuffer); + expect(result).not.toBeNull(); + [serverSendLength, _serverSendInfo] = result!; + // Client will accept the server's bad certificate due to the verify callback + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + // Because the custom verify callback overrides the default verification function + // The client connection is considered established + expect(clientConn.isEstablished()).toBeTrue(); + const clientPeerCertChain = clientConn.peerCertChain()!; + expect(clientPeerCertChain).not.toBeNull(); + expect(clientPeerCertChain).toHaveLength(1); + expect(typeof utils.derToPEM(clientPeerCertChain[0])).toBe('string'); + // We can imagine that our verify callback fails on the bad certificate + await expect( + verifyCallback(clientPeerCertChain, serverConfig.ca), + ).resolves.toBe(CryptoError.BadCertificate); + // Due to an upstream bug, if we were to simulate a close with CryptoError.UnknownCA code + // it would actually break the server connection, the client connection + // would successfully drain and then close, but the server connection is + // left to idle until it times out. + // Therefore instead of closing immediately here, we have to complete the + // handshake by sending a handshake frame to the server, and then + // simulate a close with CryptoError.UnknownCA as the code + }); + test('client -handshake-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderHandshake = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + // Simulate a CryptoError.BadCertificate as it means the client supplied a bad certificate + clientConn.close(false, CryptoError.BadCertificate, Buffer.from('')); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client has local error TlsFail CryptoError.BadCertificate', async () => { + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client -short-> server', async () => { + const result = clientConn.send(clientBuffer); + expect(result).not.toBeNull(); + [clientSendLength, _clientSendInfo] = result!; + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('server has peer error TlsFail CryptoError.BadCertificate', async () => { + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: CryptoError.BadCertificate, + reason: new Uint8Array(), + }); + }); + test('client and server close', async () => { + expect(clientConn.send(clientBuffer)).toBeNull(); + expect(serverConn.send(serverBuffer)).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + }); +}); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index f05e6117..9f6236bf 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -72,27 +72,27 @@ describe('utils', () => { }); test('to canonical IP address', () => { // IPv4 -> IPv4 - expect(utils.toCanonicalIp('127.0.0.1')).toBe('127.0.0.1'); - expect(utils.toCanonicalIp('0.0.0.0')).toBe('0.0.0.0'); - expect(utils.toCanonicalIp('255.255.255.255')).toBe('255.255.255.255'); - expect(utils.toCanonicalIp('74.125.43.99')).toBe('74.125.43.99'); + expect(utils.toCanonicalIP('127.0.0.1')).toBe('127.0.0.1'); + expect(utils.toCanonicalIP('0.0.0.0')).toBe('0.0.0.0'); + expect(utils.toCanonicalIP('255.255.255.255')).toBe('255.255.255.255'); + expect(utils.toCanonicalIP('74.125.43.99')).toBe('74.125.43.99'); // IPv4 mapped hex -> IPv4 - expect(utils.toCanonicalIp('::ffff:7f00:1')).toBe('127.0.0.1'); - expect(utils.toCanonicalIp('::ffff:0:0')).toBe('0.0.0.0'); - expect(utils.toCanonicalIp('::ffff:ffff:ffff')).toBe('255.255.255.255'); - expect(utils.toCanonicalIp('::ffff:4a7d:2b63')).toBe('74.125.43.99'); + expect(utils.toCanonicalIP('::ffff:7f00:1')).toBe('127.0.0.1'); + expect(utils.toCanonicalIP('::ffff:0:0')).toBe('0.0.0.0'); + expect(utils.toCanonicalIP('::ffff:ffff:ffff')).toBe('255.255.255.255'); + expect(utils.toCanonicalIP('::ffff:4a7d:2b63')).toBe('74.125.43.99'); // IPv4 mapped dec -> IPv4 - expect(utils.toCanonicalIp('::ffff:127.0.0.1')).toBe('127.0.0.1'); - expect(utils.toCanonicalIp('::ffff:0.0.0.0')).toBe('0.0.0.0'); - expect(utils.toCanonicalIp('::ffff:255.255.255.255')).toBe( + expect(utils.toCanonicalIP('::ffff:127.0.0.1')).toBe('127.0.0.1'); + expect(utils.toCanonicalIP('::ffff:0.0.0.0')).toBe('0.0.0.0'); + expect(utils.toCanonicalIP('::ffff:255.255.255.255')).toBe( '255.255.255.255', ); - expect(utils.toCanonicalIp('::ffff:74.125.43.99')).toBe('74.125.43.99'); + expect(utils.toCanonicalIP('::ffff:74.125.43.99')).toBe('74.125.43.99'); // IPv6 -> IPv6 - expect(utils.toCanonicalIp('::1234:7f00:1')).toBe('::1234:7f00:1'); - expect(utils.toCanonicalIp('::1234:0:0')).toBe('::1234:0:0'); - expect(utils.toCanonicalIp('::1234:ffff:ffff')).toBe('::1234:ffff:ffff'); - expect(utils.toCanonicalIp('::1234:4a7d:2b63')).toBe('::1234:4a7d:2b63'); + expect(utils.toCanonicalIP('::1234:7f00:1')).toBe('::1234:7f00:1'); + expect(utils.toCanonicalIP('::1234:0:0')).toBe('::1234:0:0'); + expect(utils.toCanonicalIP('::1234:ffff:ffff')).toBe('::1234:ffff:ffff'); + expect(utils.toCanonicalIP('::1234:4a7d:2b63')).toBe('::1234:4a7d:2b63'); }); test('resolves zero IP to local IP', () => { expect(utils.resolvesZeroIP('0.0.0.0' as Host)).toBe('127.0.0.1'); diff --git a/tests/utils.ts b/tests/utils.ts index d75ebf2e..f0118d6b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -5,21 +5,17 @@ import type QUICClient from '@/QUICClient'; import type QUICServer from '@/QUICServer'; import type QUICStream from '@/QUICStream'; import type { StreamCodeToReason, StreamReasonToCode } from '@'; -import { Crypto } from '@peculiar/webcrypto'; +import * as peculiarWebcrypto from '@peculiar/webcrypto'; import * as x509 from '@peculiar/x509'; -import { never } from '@/utils'; +import * as nobleEd25519 from '@noble/ed25519'; +import fc from 'fast-check'; /** * WebCrypto polyfill from @peculiar/webcrypto * This behaves differently with respect to Ed25519 keys * See: https://github.com/PeculiarVentures/webcrypto/issues/55 */ -const webcrypto = new Crypto(); - -/** - * Monkey patches the global crypto object polyfill - */ -globalThis.crypto = webcrypto; +const webcrypto = new peculiarWebcrypto.Crypto(); x509.cryptoProvider.set(webcrypto); @@ -27,6 +23,10 @@ async function sleep(ms: number): Promise { return await new Promise((r) => setTimeout(r, ms)); } +async function yieldMicro(): Promise { + return await new Promise((r) => queueMicrotask(r)); +} + async function randomBytes(data: ArrayBuffer) { webcrypto.getRandomValues(new Uint8Array(data)); } @@ -97,6 +97,49 @@ async function generateKeyPairEd25519(): Promise<{ }; } +async function publicKeyFromPrivateKey( + privateKey: JsonWebKey, +): Promise { + switch (privateKey.kty) { + case 'RSA': + return { + kty: privateKey.kty, + alg: privateKey.alg, + key_ops: ['verify'], + ext: privateKey.ext, + n: privateKey.n, + e: privateKey.e, + }; + case 'EC': + return { + kty: privateKey.kty, + crv: privateKey.crv, + key_ops: ['verify'], + ext: true, + x: privateKey.x, + y: privateKey.y, + }; + case 'OKP': { + // For Ed25519 keys generated by @peculiar/webcrypto, + // we cannot just take the `privateKey.x` as the public key + // The property does not exist on the JWK, so we have to + // calculate it using @noble/ed25519 + const publicKey = await nobleEd25519.getPublicKey( + new Uint8Array(Buffer.from(privateKey.d!, 'base64url')), + ); + return { + kty: privateKey.kty, + crv: privateKey.crv, + key_ops: ['verify'], + ext: true, + x: Buffer.from(publicKey).toString('base64url'), + }; + } + default: + throw new Error(`Unsupported key type ${privateKey.kty}`); + } +} + /** * Imports public key. * This uses `@peculiar/webcrypto` API for Ed25519 keys. @@ -366,9 +409,6 @@ async function generateCertificate({ issuerAttrsExtra?: Array<{ [key: string]: Array }>; now?: Date; }): Promise { - const certIdNum = parseInt(certId); - const iss = certIdNum === 0 ? certIdNum : certIdNum - 1; - const sub = certIdNum; const subjectPublicCryptoKey = await importPublicKey( subjectKeyPair.publicKey, ); @@ -393,7 +433,19 @@ async function generateCertificate({ if (notAfterDate > new Date(new Date('2050').getTime() - 1)) { throw new RangeError('`notAfterDate` cannot be after 2049-12-31T23:59:59Z'); } + const subjectNodeId = await webcrypto.subtle.digest( + 'SHA-256', + await webcrypto.subtle.exportKey('spki', subjectPublicCryptoKey), + ); + const issuerPublicKey = await publicKeyFromPrivateKey(issuerPrivateKey); + const issuerPublicCryptoKey = await importPublicKey(issuerPublicKey); + const issuerNodeId = await webcrypto.subtle.digest( + 'SHA-256', + await webcrypto.subtle.exportKey('spki', issuerPublicCryptoKey), + ); const serialNumber = certId; + const subjectNodeIdEncoded = Buffer.from(subjectNodeId).toString('hex'); + const issuerNodeIdEncoded = Buffer.from(issuerNodeId).toString('hex'); // The entire subject attributes and issuer attributes // is constructed via `x509.Name` class // By default this supports on a limited set of names: @@ -404,14 +456,14 @@ async function generateCertificate({ // Because the OID is what is encoded into ASN.1 const subjectAttrs = [ { - CN: [`${sub}`], + CN: [subjectNodeIdEncoded], }, // Filter out conflicting CN attributes ...subjectAttrsExtra.filter((attr) => !('CN' in attr)), ]; const issuerAttrs = [ { - CN: [`${iss}`], + CN: [issuerNodeIdEncoded], }, // Filter out conflicting CN attributes ...issuerAttrsExtra.filter((attr) => !('CN' in attr)), @@ -445,7 +497,7 @@ async function generateCertificate({ publicKey: subjectPublicCryptoKey, signingKey: subjectPrivateCryptoKey, extensions: [ - new x509.BasicConstraintsExtension(true, undefined, true), + new x509.BasicConstraintsExtension(true), new x509.KeyUsagesExtension( x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign | @@ -454,7 +506,6 @@ async function generateCertificate({ x509.KeyUsageFlags.keyAgreement | x509.KeyUsageFlags.keyEncipherment | x509.KeyUsageFlags.dataEncipherment, - true, ), new x509.ExtendedKeyUsageExtension([ extendedKeyUsageFlags.serverAuth, @@ -464,6 +515,36 @@ async function generateCertificate({ extendedKeyUsageFlags.timeStamping, extendedKeyUsageFlags.ocspSigning, ]), + new x509.SubjectAlternativeNameExtension([ + { + type: 'dns', + value: subjectNodeIdEncoded, + }, + { + type: 'dns', + value: 'localhost', + }, + // Quiche doesn't support IP SANs, + // instead we hack these in as DNS SANs for testing purposes + { + type: 'dns', + value: '127.0.0.1', + }, + // Quiche doesn't support IP SANs, + // instead we hack these in as DNS SANs for testing purposes + { + type: 'dns', + value: '::1', + }, + { + type: 'ip', + value: '127.0.0.1', + }, + { + type: 'ip', + value: '::1', + }, + ]), await x509.SubjectKeyIdentifierExtension.create(subjectPublicCryptoKey), ] as Array, }; @@ -539,14 +620,55 @@ async function verifyHMAC( } /** - * Use this on every client or server. It is essential for cleaning them up. + * Zero-copy wraps ArrayBuffer-like objects into Buffer + * This supports ArrayBuffer, TypedArrays and the NodeJS Buffer */ -function extractSocket( - thing: QUICClient | QUICServer, - sockets: Set, -) { - // @ts-ignore: kidnap protected property - sockets.add(thing.socket); +function bufferWrap( + array: BufferSource, + offset?: number, + length?: number, +): Buffer { + if (Buffer.isBuffer(array)) { + return array; + } else if (ArrayBuffer.isView(array)) { + return Buffer.from( + array.buffer, + offset ?? array.byteOffset, + length ?? array.byteLength, + ); + } else { + return Buffer.from(array, offset, length); + } +} + +const bufferArb = (constraints?: fc.IntArrayConstraints) => { + return fc.uint8Array(constraints).map(bufferWrap); +}; + +/** + * Creates two socket handling functions. + * `extractSocket` is used to extract a socket out of a client or server. + * `stopSockets` is used to stop all sockets that were extracted. + * + * This is used as a failsafe cleanup for active sockets. + * Failing to clean up sockets in a test will cause CI jobs to hang for 1 hour. + */ +function socketCleanupFactory() { + const sockets = new Set(); + return { + extractSocket: (thing: QUICClient | QUICServer) => { + // @ts-ignore: kidnap protected property + sockets.add(thing.socket); + }, + stopSockets: async () => { + const stopProms: Array> = []; + for (const socket of sockets) { + stopProms.push(socket.stop({ force: true })); + } + await Promise.all(stopProms); + }, + sockets, + }; } type Messages = Array; @@ -633,64 +755,75 @@ timeout: ${conn.timeout()}, `; } -type KeyTypes = 'RSA' | 'ECDSA' | 'ED25519'; +type KeyTypes = 'RSA' | 'ECDSA' | 'Ed25519'; type TLSConfigs = { - key: string; - cert: string; - ca: string; + leafKeyPair: { publicKey: JsonWebKey; privateKey: JsonWebKey }; + leafKeyPairPEM: { publicKey: string; privateKey: string }; + leafCert: X509Certificate; + leafCertPEM: string; + caKeyPair: { publicKey: JsonWebKey; privateKey: JsonWebKey }; + caKeyPairPEM: { publicKey: string; privateKey: string }; + caCert: X509Certificate; + caCertPEM: string; }; -async function generateConfig(type: KeyTypes): Promise { - let privateKeyPem: string; - let keysLeaf: { publicKey: JsonWebKey; privateKey: JsonWebKey }; - let keysCa: { publicKey: JsonWebKey; privateKey: JsonWebKey }; +async function generateTLSConfig(type: KeyTypes): Promise { + let leafKeyPair: { publicKey: JsonWebKey; privateKey: JsonWebKey }; + let leafKeyPairPEM: { publicKey: string; privateKey: string }; + let caKeyPair: { publicKey: JsonWebKey; privateKey: JsonWebKey }; + let caKeyPairPEM: { publicKey: string; privateKey: string }; switch (type) { case 'RSA': { - keysLeaf = await generateKeyPairRSA(); - keysCa = await generateKeyPairRSA(); - privateKeyPem = (await keyPairRSAToPEM(keysLeaf)).privateKey; + leafKeyPair = await generateKeyPairRSA(); + leafKeyPairPEM = await keyPairRSAToPEM(leafKeyPair); + caKeyPair = await generateKeyPairRSA(); + caKeyPairPEM = await keyPairRSAToPEM(caKeyPair); } break; case 'ECDSA': { - keysLeaf = await generateKeyPairECDSA(); - keysCa = await generateKeyPairECDSA(); - privateKeyPem = (await keyPairECDSAToPEM(keysLeaf)).privateKey; + leafKeyPair = await generateKeyPairECDSA(); + leafKeyPairPEM = await keyPairECDSAToPEM(leafKeyPair); + caKeyPair = await generateKeyPairECDSA(); + caKeyPairPEM = await keyPairECDSAToPEM(caKeyPair); } break; - case 'ED25519': + case 'Ed25519': { - keysLeaf = await generateKeyPairEd25519(); - keysCa = await generateKeyPairEd25519(); - privateKeyPem = (await keyPairEd25519ToPEM(keysLeaf)).privateKey; + leafKeyPair = await generateKeyPairEd25519(); + leafKeyPairPEM = await keyPairEd25519ToPEM(leafKeyPair); + caKeyPair = await generateKeyPairEd25519(); + caKeyPairPEM = await keyPairEd25519ToPEM(caKeyPair); } break; - default: - never(); } - - const certCa = await generateCertificate({ + const caCert = await generateCertificate({ certId: '0', - duration: 100000, - issuerPrivateKey: keysCa.privateKey, - subjectKeyPair: keysCa, + issuerPrivateKey: caKeyPair.privateKey, + subjectKeyPair: caKeyPair, + duration: 60 * 60 * 24 * 365 * 10, }); - const certLeaf = await generateCertificate({ + const leafCert = await generateCertificate({ certId: '1', - duration: 100000, - issuerPrivateKey: keysCa.privateKey, - subjectKeyPair: keysLeaf, + issuerPrivateKey: caKeyPair.privateKey, + subjectKeyPair: leafKeyPair, + duration: 60 * 60 * 24 * 365 * 10, }); return { - key: privateKeyPem, - cert: certToPEM(certLeaf), - ca: certToPEM(certCa), + leafKeyPair, + leafKeyPairPEM, + leafCert, + leafCertPEM: certToPEM(leafCert), + caKeyPair, + caKeyPairPEM, + caCert, + caCertPEM: certToPEM(caCert), }; } /** - * This will create a `reasonToCode` and `CodeToReason` function that will + * This will create a `reasonToCode` and `codeToReason` functions that will * allow errors to "jump" the network boundary. It does this by mapping the * errors to an incrementing code and returning them on the other end of the * connection. @@ -720,10 +853,14 @@ function createReasonConverters() { export { sleep, + yieldMicro, randomBytes, generateKeyPairRSA, generateKeyPairECDSA, generateKeyPairEd25519, + publicKeyFromPrivateKey, + importPublicKey, + importPrivateKey, keyPairRSAToPEM, keyPairECDSAToPEM, keyPairEd25519ToPEM, @@ -732,11 +869,13 @@ export { generateKeyHMAC, signHMAC, verifyHMAC, - extractSocket, + bufferWrap, + bufferArb, + socketCleanupFactory, handleStreamProm, waitForTimeoutNull, connStats, - generateConfig, + generateTLSConfig, createReasonConverters, };