├── .editorconfig ├── .github └── workflows │ ├── feature.yml │ ├── staging.yml │ └── tag.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── buffer_allocation.ts ├── index.ts ├── results │ ├── buffer_allocation.chart.html │ ├── buffer_allocation.json │ ├── buffer_allocation_metrics.txt │ ├── metrics.txt │ ├── stream_1KiB.chart.html │ ├── stream_1KiB.json │ ├── stream_1KiB_FFI.chart.html │ ├── stream_1KiB_FFI.json │ ├── stream_1KiB_FFI_metrics.txt │ ├── stream_1KiB_metrics.txt │ └── system.json ├── stream_1KiB.ts ├── stream_1KiB_FFI.ts └── utils │ ├── BenchHandler.ts │ ├── index.ts │ └── utils.ts ├── build.rs ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ ├── QUICClient.html │ ├── QUICConnection.html │ ├── QUICConnectionId.html │ ├── QUICConnectionMap.html │ ├── QUICServer.html │ ├── QUICSocket.html │ ├── QUICStream.html │ ├── errors.ErrorQUIC.html │ ├── errors.ErrorQUICClient.html │ ├── errors.ErrorQUICClientCreateTimeout.html │ ├── errors.ErrorQUICClientDestroyed.html │ ├── errors.ErrorQUICClientInternal.html │ ├── errors.ErrorQUICClientInvalidArgument.html │ ├── errors.ErrorQUICClientInvalidHost.html │ ├── errors.ErrorQUICClientSocketNotRunning.html │ ├── errors.ErrorQUICConfig.html │ ├── errors.ErrorQUICConnection.html │ ├── errors.ErrorQUICConnectionClosed.html │ ├── errors.ErrorQUICConnectionConfigInvalid.html │ ├── errors.ErrorQUICConnectionIdleTimeout.html │ ├── errors.ErrorQUICConnectionInternal.html │ ├── errors.ErrorQUICConnectionLocal.html │ ├── errors.ErrorQUICConnectionLocalTLS.html │ ├── errors.ErrorQUICConnectionNotRunning.html │ ├── errors.ErrorQUICConnectionPeer.html │ ├── errors.ErrorQUICConnectionPeerTLS.html │ ├── errors.ErrorQUICConnectionStartData.html │ ├── errors.ErrorQUICConnectionStartTimeout.html │ ├── errors.ErrorQUICConnectionStopping.html │ ├── errors.ErrorQUICHostInvalid.html │ ├── errors.ErrorQUICPortInvalid.html │ ├── errors.ErrorQUICServer.html │ ├── errors.ErrorQUICServerInternal.html │ ├── errors.ErrorQUICServerNewConnection.html │ ├── errors.ErrorQUICServerNotRunning.html │ ├── errors.ErrorQUICServerSocketNotRunning.html │ ├── errors.ErrorQUICServerStopping.html │ ├── errors.ErrorQUICSocket.html │ ├── errors.ErrorQUICSocketConnectionsActive.html │ ├── errors.ErrorQUICSocketInternal.html │ ├── errors.ErrorQUICSocketInvalidBindAddress.html │ ├── errors.ErrorQUICSocketInvalidSendAddress.html │ ├── errors.ErrorQUICSocketNotRunning.html │ ├── errors.ErrorQUICStream.html │ ├── errors.ErrorQUICStreamDestroyed.html │ ├── errors.ErrorQUICStreamInternal.html │ ├── errors.ErrorQUICStreamLimit.html │ ├── errors.ErrorQUICStreamLocalRead.html │ ├── errors.ErrorQUICStreamLocalWrite.html │ ├── errors.ErrorQUICStreamPeerRead.html │ ├── errors.ErrorQUICStreamPeerWrite.html │ ├── errors.ErrorQUICUndefinedBehaviour.html │ ├── events.EventQUIC.html │ ├── events.EventQUICClient.html │ ├── events.EventQUICClientClose.html │ ├── events.EventQUICClientDestroy.html │ ├── events.EventQUICClientDestroyed.html │ ├── events.EventQUICClientError.html │ ├── events.EventQUICClientErrorSend.html │ ├── events.EventQUICConnection.html │ ├── events.EventQUICConnectionClose.html │ ├── events.EventQUICConnectionError.html │ ├── events.EventQUICConnectionSend.html │ ├── events.EventQUICConnectionStart.html │ ├── events.EventQUICConnectionStarted.html │ ├── events.EventQUICConnectionStop.html │ ├── events.EventQUICConnectionStopped.html │ ├── events.EventQUICConnectionStream.html │ ├── events.EventQUICServer.html │ ├── events.EventQUICServerClose.html │ ├── events.EventQUICServerConnection.html │ ├── events.EventQUICServerError.html │ ├── events.EventQUICServerStart.html │ ├── events.EventQUICServerStarted.html │ ├── events.EventQUICServerStop.html │ ├── events.EventQUICServerStopped.html │ ├── events.EventQUICSocket.html │ ├── events.EventQUICSocketClose.html │ ├── events.EventQUICSocketError.html │ ├── events.EventQUICSocketStart.html │ ├── events.EventQUICSocketStarted.html │ ├── events.EventQUICSocketStop.html │ ├── events.EventQUICSocketStopped.html │ ├── events.EventQUICStream.html │ ├── events.EventQUICStreamCloseRead.html │ ├── events.EventQUICStreamCloseWrite.html │ ├── events.EventQUICStreamDestroy.html │ ├── events.EventQUICStreamDestroyed.html │ ├── events.EventQUICStreamError.html │ └── events.EventQUICStreamSend.html ├── enums │ ├── native.CongestionControlAlgorithm.html │ ├── native.ConnectionErrorCode.html │ ├── native.CryptoError.html │ ├── native.Shutdown.html │ └── native.Type.html ├── functions │ ├── utils.bufferWrap.html │ ├── utils.buildAddress.html │ ├── utils.collectPEMs.html │ ├── utils.decodeConnectionId.html │ ├── utils.derToPEM.html │ ├── utils.encodeConnectionId.html │ ├── utils.formatError.html │ ├── utils.fromIPv4MappedIPv6.html │ ├── utils.isHostWildcard.html │ ├── utils.isIPv4.html │ ├── utils.isIPv4MappedIPv6.html │ ├── utils.isIPv4MappedIPv6Dec.html │ ├── utils.isIPv4MappedIPv6Hex.html │ ├── utils.isIPv6.html │ ├── utils.isPort.html │ ├── utils.isStreamBidirectional.html │ ├── utils.isStreamClientInitiated.html │ ├── utils.isStreamReset.html │ ├── utils.isStreamServerInitiated.html │ ├── utils.isStreamStopped.html │ ├── utils.isStreamUnidirectional.html │ ├── utils.mintToken.html │ ├── utils.never.html │ ├── utils.pemToDER.html │ ├── utils.promise.html │ ├── utils.promisify.html │ ├── utils.resolveHost.html │ ├── utils.resolveHostname.html │ ├── utils.resolvesZeroIP.html │ ├── utils.setMaxListeners.html │ ├── utils.toCanonicalIP.html │ ├── utils.toIPv4MappedIPv6Dec.html │ ├── utils.toIPv4MappedIPv6Hex.html │ ├── utils.toPort.html │ ├── utils.validateTarget.html │ ├── utils.validateToken.html │ └── utils.yieldMicro.html ├── index.html ├── interfaces │ ├── native.Config.html │ ├── native.ConfigConstructor.html │ ├── native.Connection.html │ ├── native.ConnectionConstructor.html │ ├── native.Header.html │ ├── native.HeaderConstructor.html │ └── native.Quiche.html ├── modules.html ├── modules │ ├── errors.html │ ├── events.html │ ├── native.html │ └── utils.html ├── types │ ├── Address.html │ ├── Callback.html │ ├── Class.html │ ├── ClientCryptoOps.html │ ├── ConnectionId.html │ ├── ConnectionIdString.html │ ├── ConnectionMetadata.html │ ├── Host.html │ ├── Hostname.html │ ├── Opaque.html │ ├── Port.html │ ├── PromiseDeconstructed.html │ ├── QUICClientConfigInput.html │ ├── QUICClientCrypto.html │ ├── QUICConfig.html │ ├── QUICServerConfigInput.html │ ├── QUICServerCrypto.html │ ├── QUICStreamMap.html │ ├── RemoteInfo.html │ ├── ResolveHostname.html │ ├── ServerCryptoOps.html │ ├── StreamCodeToReason.html │ ├── StreamId.html │ ├── StreamReasonToCode.html │ ├── TLSVerifyCallback.html │ ├── native.ConnectionError.html │ ├── native.Host.html │ ├── native.HostIter.html │ ├── native.PathEvent.html │ ├── native.PathStats.html │ ├── native.PathStatsIter.html │ ├── native.QuicheTimeInstant.html │ ├── native.RecvInfo.html │ ├── native.SendInfo.html │ ├── native.Stats.html │ └── native.StreamIter.html └── variables │ ├── native.quiche-1.html │ ├── utils.textDecoder.html │ └── utils.textEncoder.html ├── examples └── test_example.rs ├── flake.lock ├── flake.nix ├── images ├── quic_connection_negotiation.svg ├── quic_dataflow.svg ├── quic_structure_encapsulated.svg └── quic_structure_injected.svg ├── jest.config.mjs ├── package-lock.json ├── package.json ├── scripts ├── brew-install.sh ├── choco-install.ps1 ├── prebuild.mjs ├── prepublishOnly.mjs ├── test.mjs └── version.mjs ├── src ├── QUICClient.ts ├── QUICConnection.ts ├── QUICConnectionId.ts ├── QUICConnectionMap.ts ├── QUICServer.ts ├── QUICSocket.ts ├── QUICStream.ts ├── config.ts ├── errors.ts ├── events.ts ├── index.ts ├── native │ ├── index.ts │ ├── napi │ │ ├── config.rs │ │ ├── connection.rs │ │ ├── constants.rs │ │ ├── lib.rs │ │ ├── packet.rs │ │ ├── path.rs │ │ └── stream.rs │ ├── quiche.ts │ └── types.ts ├── types.ts └── utils.ts ├── tests ├── QUICClient.test.ts ├── QUICConnectionId.test.ts ├── QUICServer.test.ts ├── QUICSocket.test.ts ├── QUICStream.test.ts ├── concurrency.test.ts ├── config.test.ts ├── global.d.ts ├── globalSetup.ts ├── globalTeardown.ts ├── native │ ├── connection.test.ts │ ├── quiche.test.ts │ ├── stream.test.ts │ └── tls │ │ ├── ecdsa.test.ts │ │ ├── ed25519.test.ts │ │ └── rsa.test.ts ├── setup.ts ├── setupAfterEnv.ts ├── utils.test.ts └── utils.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/workflows/feature.yml: -------------------------------------------------------------------------------- 1 | name: "CI / Feature" 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature* 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | use-native-library-js-feature: 15 | permissions: 16 | contents: read 17 | actions: write 18 | checks: write 19 | uses: MatrixAI/.github/.github/workflows/native-library-js-feature.yml@master 20 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: "CI / Staging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | use-native-library-js-staging: 15 | permissions: 16 | contents: write 17 | actions: write 18 | checks: write 19 | pull-requests: write 20 | uses: MatrixAI/.github/.github/workflows/native-library-js-staging.yml@master 21 | secrets: 22 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 23 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 24 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 25 | GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} 26 | GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: "CI / Tag" 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | use-native-library-js-tag: 15 | permissions: 16 | contents: write 17 | actions: write 18 | checks: write 19 | uses: MatrixAI/.github/.github/workflows/native-library-js-tag.yml@master 20 | secrets: 21 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /dist 3 | .env* 4 | !.env.example 5 | # nix 6 | /result* 7 | /builds 8 | # native 9 | /build 10 | /prebuild 11 | /prepublishOnly 12 | /target 13 | 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | lerna-debug.log* 22 | 23 | # Diagnostic reports (https://nodejs.org/api/report.html) 24 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | *.lcov 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (https://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | web_modules/ 60 | 61 | # TypeScript cache 62 | *.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Microbundle cache 71 | .rpt2_cache/ 72 | .rts2_cache_cjs/ 73 | .rts2_cache_es/ 74 | .rts2_cache_umd/ 75 | 76 | # Optional REPL history 77 | .node_repl_history 78 | 79 | # Output of 'npm pack' 80 | *.tgz 81 | 82 | # Yarn Integrity file 83 | .yarn-integrity 84 | 85 | # dotenv environment variables file 86 | .env 87 | .env.test 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # prettierrc for local editing 133 | .prettierrc 134 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /*.nix 3 | /nix 4 | /tsconfig.json 5 | /tsconfig.build.json 6 | /babel.config.js 7 | /jest.config.js 8 | /scripts 9 | /src 10 | /tests 11 | /tmp 12 | /docs 13 | /images 14 | /benches 15 | /build 16 | /builds 17 | /prebuild 18 | /prepublishOnly 19 | /dist/tsbuildinfo 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Enables npm link 2 | prefix=~/.npm 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quic" 3 | version = "2.0.9" 4 | authors = ["Roger Qiu "] 5 | license-file = "LICENSE" 6 | edition = "2021" 7 | exclude = ["index.node"] 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | path = "src/native/napi/lib.rs" 12 | 13 | [dependencies] 14 | napi = { version = "2", features = ["async", "napi6", "serde-json"] } 15 | serde = { version = "1.0", features = ["derive"] } 16 | napi-derive = { version = "2", default-features = false, features = ["strict", "compat-mode"] } 17 | quiche = { version = "0.18.0", features = ["boringssl-boring-crate", "boringssl-vendored"] } 18 | boring = "3" 19 | 20 | [build-dependencies] 21 | napi-build = "2" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-quic 2 | 3 | QUIC library for TypeScript/JavaScript applications. 4 | 5 | This is built on top of Cloudflare's 6 | [quiche](https://github.com/cloudflare/quiche) library. It is intended to 7 | support Linux, Windows MacOS, Android and iOS. Mobile support is still pending. 8 | 9 | Since Cloudflare's quiche is written in Rust. This uses the 10 | [napi-rs](https://github.com/napi-rs/napi-rs) binding system compile the native 11 | objects for Node.js. 12 | 13 | This library focuses only on the QUIC protocol. It does not support HTTP3. You 14 | can build HTTP3 on top of this. 15 | 16 | ## Installation 17 | 18 | ```sh 19 | npm install --save @matrixai/quic 20 | ``` 21 | 22 | ## Usage 23 | 24 | See the example usage in [tests](tests). 25 | 26 | ## Development 27 | 28 | Run `nix develop`, and once you're inside, you can use: 29 | 30 | ```sh 31 | # install (or reinstall packages from package.json) 32 | npm install 33 | # build the native objects 34 | npm run prebuild 35 | # build the dist and native objects 36 | npm run build 37 | # run the repl (this allows you to import from ./src) 38 | npm run tsx 39 | # run the tests 40 | npm run test 41 | # lint the source code 42 | npm run lint 43 | # automatically fix the source 44 | npm run lintfix 45 | ``` 46 | 47 | ### Docs Generation 48 | 49 | ```sh 50 | npm run docs 51 | ``` 52 | 53 | See the docs at: https://matrixai.github.io/js-quic/ 54 | 55 | ### Publishing 56 | 57 | ```sh 58 | # npm login 59 | npm version patch # major/minor/patch 60 | npm run build 61 | npm publish --access public 62 | git push 63 | git push --tags 64 | ``` 65 | 66 | ## How it works 67 | 68 | ### Quiche 69 | 70 | To understand how to develop this, it is important to understand how quiche 71 | works. 72 | 73 | Clone the https://github.com/cloudflare/quiche project. It's multi-workspace 74 | Cargo project. 75 | 76 | You can build and run their examples located in `/quiche/examples/`: 77 | 78 | ```sh 79 | cargo build --examples 80 | cargo run --example client '127.0.0.1:55555' 81 | ``` 82 | 83 | You can run their apps located in `/apps/src/bin`: 84 | 85 | ```sh 86 | cd /apps 87 | 88 | # The source code for these is in the `/apps/src/bin` directory 89 | cargo run --bin quiche-client -- https://cloudflare-quic.com 90 | 91 | # Run with 92 | cargo run --bin quiche-server -- --listen 127.0.0.1:55555 93 | 94 | # Run without verifying TLS if certificates is 95 | cargo run --bin quiche-client -- --no-verify 'http://127.0.0.1:55555' 96 | ``` 97 | 98 | ### TLS 99 | 100 | If you need to test with a local certificates, try using `step`; 101 | 102 | ```sh 103 | step certificate create \ 104 | localhost localhost.crt localhost.key \ 105 | --profile self-signed \ 106 | --subtle \ 107 | --no-password \ 108 | --insecure \ 109 | --force \ 110 | --san 127.0.0.1 \ 111 | --san ::1 \ 112 | --not-after 31536000s 113 | 114 | # Afterwards put certificates in `./tmp` and refer to them 115 | ``` 116 | 117 | ### Cargo/Rust targets 118 | 119 | Cargo is a cross-compiler. The target structure looks like this: 120 | 121 | ``` 122 | --- 123 | ``` 124 | 125 | For example: 126 | 127 | ``` 128 | x86_64-unknown-linux-gnu 129 | x86_64-pc-windows-msvc 130 | aarch64-apple-darwin 131 | x86_64-apple-darwin 132 | ``` 133 | 134 | The available target list is in `rustc --print target-list`. 135 | 136 | ### Structure 137 | 138 | It is possible to structure the QUIC system in the encapsulated way or the 139 | injected way. 140 | 141 | When using the encapsulated way, the `QUICSocket` is separated between client 142 | and server. 143 | 144 | When using the injected way, the `QUICSocket` is shared between client and 145 | server. 146 | 147 | ![image](/images/quic_structure_encapsulated.svg) 148 | 149 | If you are building a peer to peer network, you must use the injected way. This 150 | is the only way to ensure that hole-punching works because both the client and 151 | server for any given peer must share the same UDP socket and thus share the 152 | `QUICSocket`. When done in this way, the `QUICSocket` lifecycle is managed 153 | outside of both the `QUICClient` and `QUICServer`. 154 | 155 | ![image](/images/quic_structure_injected.svg) 156 | 157 | This also means both `QUICClient` and `QUICServer` must share the same 158 | connection map. In order to allow the `QUICSocket` to dispatch data into the 159 | correct connection, the connection map is constructed in the `QUICSocket`, 160 | however setting and unsetting connections is managed by `QUICClient` and 161 | `QUICServer`. 162 | 163 | ### Dataflow 164 | 165 | The data flow of the QUIC system is a bidirectional graph. 166 | 167 | Data received from the outside world is received on the UDP socket. It is parsed 168 | and then dispatched to each `QUICConnection`. Each connection further parses the 169 | data and then dispatches to the `QUICStream`. Each `QUICStream` presents the 170 | data on the `ReadableStream` interface, which can be read by a caller. 171 | 172 | Data sent to the outside world is written to a `WritableStream` interface of a 173 | `QUICStream`. This data is buffered up in the underlying Quiche stream. A send 174 | procedure is triggered on the associated `QUICConnection` which takes all the 175 | buffered data to be sent for that connection, and sends it to the `QUICSocket`, 176 | which then sends it to the underlying UDP socket. 177 | 178 | ![image](/images/quic_dataflow.svg) 179 | 180 | Buffering occurs at the connection level and at the stream level. Each 181 | connection has a global buffer for all streams, and each stream has its own 182 | buffer. Note that connection buffering and stream buffering all occur within the 183 | Quiche library. The web streams `ReadableStream` and `WritableStream` do not do 184 | any buffering at all. 185 | 186 | ### Connection Negotiation 187 | 188 | The connection negotiation process involves several exchanges of QUIC packets 189 | before the `QUICConnection` is constructed. 190 | 191 | The primary reason to do this is for both sides to determine their respective 192 | connection IDs. 193 | 194 | ![image](/images/quic_connection_negotiation.svg) 195 | 196 | ### Push & Pull 197 | 198 | The `QUICSocket`, `QUICClient`, `QUICServer`, `QUICConnection` and `QUICStream` 199 | are independent state machines that exposes methods that can be called as well 200 | as events that may be emitted between them. 201 | 202 | This creates a concurrent decentralised state machine system where there are 203 | multiple entrypoints of change. 204 | 205 | Users may call methods which causes state transitions internally that trigger 206 | event emissions. However some methods are considered internal to the library, 207 | this means these methods are not intended to be called by the end user. They are 208 | however public relative to the other components in the system. These methods 209 | should be marked with `@internal` documentation annotation. 210 | 211 | External events may also trigger event handlers that will call methods which 212 | perform state transitions and event emission. 213 | 214 | Keeping track of how the system works is therefore quite complex and must follow 215 | a set of rules. 216 | 217 | - Pull methods - these are either synchronous or asynchronous methods that may 218 | throw exceptions. 219 | - Push handlers - these are event handlers that can initiate pull methods, if 220 | these pull handlers throw exceptions, these exceptions must be caught, and 221 | expected runtime exceptions are to be converted to error events, all other 222 | exceptions will be considered to be software bugs and will be bubbled up to 223 | the program boundary as unhandled exceptions or unhandled promise rejections. 224 | Generally the only exceptions that are expected runtime exceptions are those 225 | that arise from perform IO with the operating system. 226 | 227 | ## License 228 | 229 | js-quic is licensed under Apache-2.0, you may read the terms of the license 230 | [here](LICENSE). 231 | -------------------------------------------------------------------------------- /benches/buffer_allocation.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import url from 'node:url'; 3 | import b from 'benny'; 4 | import { suiteCommon } from './utils/index.js'; 5 | 6 | const filePath = url.fileURLToPath(import.meta.url); 7 | 8 | async function main() { 9 | const summary = await b.suite( 10 | path.basename(filePath, path.extname(filePath)), 11 | b.add('Buffer.alloc', () => { 12 | Buffer.alloc(1350); 13 | }), 14 | b.add('Buffer.allocUnsafe', () => { 15 | Buffer.allocUnsafe(1350); 16 | }), 17 | b.add('Buffer.allocUnsafeSlow', () => { 18 | Buffer.allocUnsafeSlow(1350); 19 | }), 20 | b.add('Buffer.from subarray', () => { 21 | const b = Buffer.allocUnsafe(1350); 22 | return () => { 23 | Buffer.from(b.subarray(0, b.byteLength)); 24 | }; 25 | }), 26 | b.add('Buffer.copyBytesFrom', () => { 27 | const b = Buffer.allocUnsafe(1350); 28 | return () => { 29 | Buffer.copyBytesFrom(b, 0, b.byteLength); 30 | }; 31 | }), 32 | b.add('Uint8Array', () => { 33 | new Uint8Array(1350); 34 | }), 35 | b.add('Uint8Array slice', () => { 36 | const b = new Uint8Array(1350); 37 | return () => { 38 | b.slice(0, b.byteLength); 39 | }; 40 | }), 41 | ...suiteCommon, 42 | ); 43 | return summary; 44 | } 45 | 46 | if (import.meta.url.startsWith('file:')) { 47 | const modulePath = url.fileURLToPath(import.meta.url); 48 | if (process.argv[1] === modulePath) { 49 | void main(); 50 | } 51 | } 52 | 53 | export default main; 54 | -------------------------------------------------------------------------------- /benches/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | import url from 'node:url'; 6 | import si from 'systeminformation'; 7 | import { benchesPath } from './utils/utils.js'; 8 | import buffer_allocation from './buffer_allocation.js'; 9 | import stream_1KiB from './stream_1KiB.js'; 10 | import stream_1KiB_FFI from './stream_1KiB_FFI.js'; 11 | 12 | async function main(): Promise { 13 | await fs.promises.mkdir(path.join(benchesPath, 'results'), { 14 | recursive: true, 15 | }); 16 | await buffer_allocation(); 17 | await stream_1KiB(); 18 | await stream_1KiB_FFI(); 19 | const resultFilenames = await fs.promises.readdir( 20 | path.join(benchesPath, 'results'), 21 | ); 22 | const metricsFile = await fs.promises.open( 23 | path.join(benchesPath, 'results', 'metrics.txt'), 24 | 'w', 25 | ); 26 | let concatenating = false; 27 | for (const resultFilename of resultFilenames) { 28 | if (/.+_metrics\.txt$/.test(resultFilename)) { 29 | const metricsData = await fs.promises.readFile( 30 | path.join(benchesPath, 'results', resultFilename), 31 | ); 32 | if (concatenating) { 33 | await metricsFile.write('\n'); 34 | } 35 | await metricsFile.write(metricsData); 36 | concatenating = true; 37 | } 38 | } 39 | await metricsFile.close(); 40 | const systemData = await si.get({ 41 | cpu: '*', 42 | osInfo: 'platform, distro, release, kernel, arch', 43 | system: 'model, manufacturer', 44 | }); 45 | await fs.promises.writeFile( 46 | path.join(benchesPath, 'results', 'system.json'), 47 | JSON.stringify(systemData, null, 2), 48 | ); 49 | } 50 | 51 | if (import.meta.url.startsWith('file:')) { 52 | const modulePath = url.fileURLToPath(import.meta.url); 53 | if (process.argv[1] === modulePath) { 54 | void main(); 55 | } 56 | } 57 | 58 | export default main; 59 | -------------------------------------------------------------------------------- /benches/results/buffer_allocation.chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | buffer_allocation 9 | 28 | 29 | 30 |
31 | 32 |
33 | 115 | 116 | -------------------------------------------------------------------------------- /benches/results/buffer_allocation_metrics.txt: -------------------------------------------------------------------------------- 1 | # TYPE buffer_allocation_ops gauge 2 | buffer_allocation_ops{name="Buffer.alloc"} 938109 3 | buffer_allocation_ops{name="Buffer.allocUnsafe"} 4536822 4 | buffer_allocation_ops{name="Buffer.allocUnsafeSlow"} 1033223 5 | buffer_allocation_ops{name="Buffer.from subarray"} 2772304 6 | buffer_allocation_ops{name="Buffer.copyBytesFrom"} 664578 7 | buffer_allocation_ops{name="Uint8Array"} 942396 8 | buffer_allocation_ops{name="Uint8Array slice"} 967255 9 | 10 | # TYPE buffer_allocation_margin gauge 11 | buffer_allocation_margin{name="Buffer.alloc"} 1.98 12 | buffer_allocation_margin{name="Buffer.allocUnsafe"} 1.93 13 | buffer_allocation_margin{name="Buffer.allocUnsafeSlow"} 2.71 14 | buffer_allocation_margin{name="Buffer.from subarray"} 2.34 15 | buffer_allocation_margin{name="Buffer.copyBytesFrom"} 2.79 16 | buffer_allocation_margin{name="Uint8Array"} 2.16 17 | buffer_allocation_margin{name="Uint8Array slice"} 2.32 18 | 19 | # TYPE buffer_allocation_samples counter 20 | buffer_allocation_samples{name="Buffer.alloc"} 76 21 | buffer_allocation_samples{name="Buffer.allocUnsafe"} 87 22 | buffer_allocation_samples{name="Buffer.allocUnsafeSlow"} 60 23 | buffer_allocation_samples{name="Buffer.from subarray"} 91 24 | buffer_allocation_samples{name="Buffer.copyBytesFrom"} 73 25 | buffer_allocation_samples{name="Uint8Array"} 43 26 | buffer_allocation_samples{name="Uint8Array slice"} 46 27 | -------------------------------------------------------------------------------- /benches/results/metrics.txt: -------------------------------------------------------------------------------- 1 | # TYPE buffer_allocation_ops gauge 2 | buffer_allocation_ops{name="Buffer.alloc"} 938109 3 | buffer_allocation_ops{name="Buffer.allocUnsafe"} 4536822 4 | buffer_allocation_ops{name="Buffer.allocUnsafeSlow"} 1033223 5 | buffer_allocation_ops{name="Buffer.from subarray"} 2772304 6 | buffer_allocation_ops{name="Buffer.copyBytesFrom"} 664578 7 | buffer_allocation_ops{name="Uint8Array"} 942396 8 | buffer_allocation_ops{name="Uint8Array slice"} 967255 9 | 10 | # TYPE buffer_allocation_margin gauge 11 | buffer_allocation_margin{name="Buffer.alloc"} 1.98 12 | buffer_allocation_margin{name="Buffer.allocUnsafe"} 1.93 13 | buffer_allocation_margin{name="Buffer.allocUnsafeSlow"} 2.71 14 | buffer_allocation_margin{name="Buffer.from subarray"} 2.34 15 | buffer_allocation_margin{name="Buffer.copyBytesFrom"} 2.79 16 | buffer_allocation_margin{name="Uint8Array"} 2.16 17 | buffer_allocation_margin{name="Uint8Array slice"} 2.32 18 | 19 | # TYPE buffer_allocation_samples counter 20 | buffer_allocation_samples{name="Buffer.alloc"} 76 21 | buffer_allocation_samples{name="Buffer.allocUnsafe"} 87 22 | buffer_allocation_samples{name="Buffer.allocUnsafeSlow"} 60 23 | buffer_allocation_samples{name="Buffer.from subarray"} 91 24 | buffer_allocation_samples{name="Buffer.copyBytesFrom"} 73 25 | buffer_allocation_samples{name="Uint8Array"} 43 26 | buffer_allocation_samples{name="Uint8Array slice"} 46 27 | 28 | # TYPE stream_1KiB_FFI_ops gauge 29 | stream_1KiB_FFI_ops{name="send 1Kib of data over quiche FFI with no UDP socket"} 10438 30 | 31 | # TYPE stream_1KiB_FFI_margin gauge 32 | stream_1KiB_FFI_margin{name="send 1Kib of data over quiche FFI with no UDP socket"} 2.52 33 | 34 | # TYPE stream_1KiB_FFI_samples counter 35 | stream_1KiB_FFI_samples{name="send 1Kib of data over quiche FFI with no UDP socket"} 78 36 | 37 | # TYPE stream_1KiB_ops gauge 38 | stream_1KiB_ops{name="send 1Kib of data over QUICStream with UDP socket"} 1490 39 | 40 | # TYPE stream_1KiB_margin gauge 41 | stream_1KiB_margin{name="send 1Kib of data over QUICStream with UDP socket"} 9.12 42 | 43 | # TYPE stream_1KiB_samples counter 44 | stream_1KiB_samples{name="send 1Kib of data over QUICStream with UDP socket"} 36 45 | -------------------------------------------------------------------------------- /benches/results/stream_1KiB.chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | stream_1KiB 9 | 28 | 29 | 30 |
31 | 32 |
33 | 115 | 116 | -------------------------------------------------------------------------------- /benches/results/stream_1KiB.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream_1KiB", 3 | "date": "2025-03-27T21:42:52.808Z", 4 | "version": "2.0.5", 5 | "results": [ 6 | { 7 | "name": "send 1Kib of data over QUICStream with UDP socket", 8 | "ops": 1490, 9 | "margin": 9.12, 10 | "options": { 11 | "delay": 0.005, 12 | "initCount": 1, 13 | "minTime": 0.05, 14 | "maxTime": 5, 15 | "minSamples": 5 16 | }, 17 | "samples": 36, 18 | "promise": true, 19 | "details": { 20 | "min": 0.0004348170727272727, 21 | "max": 0.0012000546863636364, 22 | "mean": 0.0006711843899662148, 23 | "median": 0.0006380742022727273, 24 | "standardDeviation": 0.00018741787937670817, 25 | "marginOfError": 0.00006122317392972466, 26 | "relativeMarginOfError": 9.121662369532526, 27 | "standardErrorOfMean": 0.00003123631322945136, 28 | "sampleVariance": 3.512546151006233e-8, 29 | "sampleResults": [ 30 | 0.0004348170727272727, 31 | 0.00046142885, 32 | 0.0004623692, 33 | 0.00046806887727272726, 34 | 0.00047831012727272726, 35 | 0.00048768159090909087, 36 | 0.0005054606227272727, 37 | 0.0005107346090909091, 38 | 0.0005113952863636364, 39 | 0.0005192053727272727, 40 | 0.0005208448545454545, 41 | 0.0005216718727272727, 42 | 0.0005281841681818182, 43 | 0.0005530144772727272, 44 | 0.0005638123689320388, 45 | 0.0006028281772727272, 46 | 0.0006254643186813187, 47 | 0.0006260789136363637, 48 | 0.0006500694909090909, 49 | 0.0006635088392857142, 50 | 0.0006689604636363637, 51 | 0.0007065507454545454, 52 | 0.0007074399863636363, 53 | 0.0007520088181818183, 54 | 0.0007566928318181818, 55 | 0.0007746712967032967, 56 | 0.0007758058793103449, 57 | 0.0007856770045454545, 58 | 0.0008039686637931034, 59 | 0.0008095417136363636, 60 | 0.0008323895357142858, 61 | 0.0009133694636363636, 62 | 0.0009222795954545455, 63 | 0.0010206856590909091, 64 | 0.0010375926045454544, 65 | 0.0012000546863636364 66 | ] 67 | }, 68 | "completed": true, 69 | "percentSlower": 0 70 | } 71 | ], 72 | "fastest": { 73 | "name": "send 1Kib of data over QUICStream with UDP socket", 74 | "index": 0 75 | }, 76 | "slowest": { 77 | "name": "send 1Kib of data over QUICStream with UDP socket", 78 | "index": 0 79 | } 80 | } -------------------------------------------------------------------------------- /benches/results/stream_1KiB_FFI.chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | stream_1KiB_FFI 9 | 28 | 29 | 30 |
31 | 32 |
33 | 115 | 116 | -------------------------------------------------------------------------------- /benches/results/stream_1KiB_FFI.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream_1KiB_FFI", 3 | "date": "2025-03-27T21:42:59.487Z", 4 | "version": "2.0.5", 5 | "results": [ 6 | { 7 | "name": "send 1Kib of data over quiche FFI with no UDP socket", 8 | "ops": 10438, 9 | "margin": 2.52, 10 | "options": { 11 | "delay": 0.005, 12 | "initCount": 1, 13 | "minTime": 0.05, 14 | "maxTime": 5, 15 | "minSamples": 5 16 | }, 17 | "samples": 78, 18 | "promise": true, 19 | "details": { 20 | "min": 0.00008282045292207792, 21 | "max": 0.00014318661394557822, 22 | "mean": 0.00009580204656998795, 23 | "median": 0.00009360364149120994, 24 | "standardDeviation": 0.000010882348408672501, 25 | "marginOfError": 0.0000024150793034171726, 26 | "relativeMarginOfError": 2.5209057529400924, 27 | "standardErrorOfMean": 0.000001232183318069986, 28 | "sampleVariance": 1.1842550688773693e-10, 29 | "sampleResults": [ 30 | 0.00008282045292207792, 31 | 0.00008421432305194806, 32 | 0.0000847867012987013, 33 | 0.00008542942404006678, 34 | 0.00008557566611018365, 35 | 0.00008600833843537415, 36 | 0.00008602869115191987, 37 | 0.00008604030217028381, 38 | 0.0000865633572621035, 39 | 0.00008678823706176961, 40 | 0.00008682590651085142, 41 | 0.00008705475792988313, 42 | 0.00008708931886477462, 43 | 0.00008709259432387311, 44 | 0.00008713083163265307, 45 | 0.00008728034390651084, 46 | 0.00008812568280467446, 47 | 0.00008878670784641069, 48 | 0.00008914109348914857, 49 | 0.00008954608514190317, 50 | 0.00008954831719532555, 51 | 0.00008956611904761904, 52 | 0.00008969917346938776, 53 | 0.00008984159013605443, 54 | 0.00008991733503401361, 55 | 0.00009025532653061224, 56 | 0.00009035410876623377, 57 | 0.00009037925, 58 | 0.00009054120534223706, 59 | 0.00009118485141903173, 60 | 0.00009123160714285714, 61 | 0.00009180809515859766, 62 | 0.00009194406802721088, 63 | 0.00009214969557823129, 64 | 0.00009222912925170068, 65 | 0.00009284613095238095, 66 | 0.00009289088945578232, 67 | 0.0000935867879799666, 68 | 0.00009360210183639399, 69 | 0.00009360518114602588, 70 | 0.00009365653049907578, 71 | 0.00009413360100166944, 72 | 0.00009434931977818854, 73 | 0.00009477015692821369, 74 | 0.00009491092791127542, 75 | 0.00009493814417744917, 76 | 0.00009493972278911563, 77 | 0.00009522262270450751, 78 | 0.00009531398469387756, 79 | 0.00009592057586837294, 80 | 0.00009664668447412353, 81 | 0.0000974629482439926, 82 | 0.00009756380102040817, 83 | 0.00009767129020332717, 84 | 0.0000980790425170068, 85 | 0.00009842069897959183, 86 | 0.00009940895492487479, 87 | 0.00009943061737523104, 88 | 0.00009946587715930902, 89 | 0.00010000542346938776, 90 | 0.00010003154761904762, 91 | 0.00010013333843537415, 92 | 0.00010022366277128546, 93 | 0.00010058219001919387, 94 | 0.00010121396494156928, 95 | 0.00010151591883116883, 96 | 0.00010218079852125693, 97 | 0.00010264513243761996, 98 | 0.00010336539186691313, 99 | 0.00010337427356746765, 100 | 0.00010409885642737896, 101 | 0.00010430380782312925, 102 | 0.00011058784523809524, 103 | 0.000117398134935305, 104 | 0.00011823540480591498, 105 | 0.00012913989455782315, 106 | 0.0001385221515711645, 107 | 0.00014318661394557822 108 | ] 109 | }, 110 | "completed": true, 111 | "percentSlower": 0 112 | } 113 | ], 114 | "fastest": { 115 | "name": "send 1Kib of data over quiche FFI with no UDP socket", 116 | "index": 0 117 | }, 118 | "slowest": { 119 | "name": "send 1Kib of data over quiche FFI with no UDP socket", 120 | "index": 0 121 | } 122 | } -------------------------------------------------------------------------------- /benches/results/stream_1KiB_FFI_metrics.txt: -------------------------------------------------------------------------------- 1 | # TYPE stream_1KiB_FFI_ops gauge 2 | stream_1KiB_FFI_ops{name="send 1Kib of data over quiche FFI with no UDP socket"} 10438 3 | 4 | # TYPE stream_1KiB_FFI_margin gauge 5 | stream_1KiB_FFI_margin{name="send 1Kib of data over quiche FFI with no UDP socket"} 2.52 6 | 7 | # TYPE stream_1KiB_FFI_samples counter 8 | stream_1KiB_FFI_samples{name="send 1Kib of data over quiche FFI with no UDP socket"} 78 9 | -------------------------------------------------------------------------------- /benches/results/stream_1KiB_metrics.txt: -------------------------------------------------------------------------------- 1 | # TYPE stream_1KiB_ops gauge 2 | stream_1KiB_ops{name="send 1Kib of data over QUICStream with UDP socket"} 1490 3 | 4 | # TYPE stream_1KiB_margin gauge 5 | stream_1KiB_margin{name="send 1Kib of data over QUICStream with UDP socket"} 9.12 6 | 7 | # TYPE stream_1KiB_samples counter 8 | stream_1KiB_samples{name="send 1Kib of data over QUICStream with UDP socket"} 36 9 | -------------------------------------------------------------------------------- /benches/results/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpu": { 3 | "manufacturer": "Intel", 4 | "brand": "Gen Intel® Core™ i7-1370P", 5 | "vendor": "Intel", 6 | "family": "6", 7 | "model": "186", 8 | "stepping": "2", 9 | "revision": "", 10 | "voltage": "", 11 | "speed": 2.22, 12 | "speedMin": 0.4, 13 | "speedMax": 5.2, 14 | "governor": "powersave", 15 | "cores": 20, 16 | "physicalCores": 14, 17 | "performanceCores": 6, 18 | "efficiencyCores": 8, 19 | "processors": 1, 20 | "socket": "", 21 | "flags": "fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb intel_pt sha_ni xsaveopt xsavec xgetbv1 xsaves split_lock_detect user_shstk avx_vnni dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp hwp_pkg_req hfi vnmi umip pku ospke waitpkg gfni vaes vpclmulqdq tme rdpid movdiri movdir64b fsrm md_clear serialize pconfig arch_lbr ibt flush_l1d arch_capabilities", 22 | "virtualization": true, 23 | "cache": { 24 | "l1d": 557056, 25 | "l1i": 720896, 26 | "l2": 11534336, 27 | "l3": 25165824 28 | } 29 | }, 30 | "osInfo": { 31 | "platform": "linux", 32 | "distro": "nixos", 33 | "release": "24.11", 34 | "kernel": "6.6.47", 35 | "arch": "x64" 36 | }, 37 | "system": { 38 | "model": "Precision 3480", 39 | "manufacturer": "Dell Inc." 40 | } 41 | } -------------------------------------------------------------------------------- /benches/stream_1KiB.ts: -------------------------------------------------------------------------------- 1 | import url from 'node:url'; 2 | import path from 'node:path'; 3 | import b from 'benny'; 4 | import Logger, { LogLevel } from '@matrixai/logger'; 5 | import { BenchHandler, suiteCommon } from './utils/index.js'; 6 | import * as testsUtils from '../tests/utils.js'; 7 | import QUICClient from '#QUICClient.js'; 8 | import QUICServer from '#QUICServer.js'; 9 | import * as events from '#events.js'; 10 | import * as utils from '#utils.js'; 11 | 12 | const filePath = url.fileURLToPath(import.meta.url); 13 | 14 | async function main() { 15 | const logger = new Logger(`stream_1KiB Bench`, LogLevel.SILENT, [ 16 | new BenchHandler(), 17 | ]); 18 | const data1KiB = Buffer.allocUnsafe(1024); 19 | const tlsConfig = await testsUtils.generateTLSConfig('RSA'); 20 | const quicServer = new QUICServer({ 21 | config: { 22 | verifyPeer: false, 23 | key: tlsConfig.leafKeyPairPEM.privateKey, 24 | cert: tlsConfig.leafCertPEM, 25 | }, 26 | crypto: { 27 | key: await testsUtils.generateKeyHMAC(), 28 | ops: { 29 | sign: testsUtils.signHMAC, 30 | verify: testsUtils.verifyHMAC, 31 | }, 32 | }, 33 | logger: logger.getChild('QUICServer'), 34 | }); 35 | const { p: serverStreamEndedP, resolveP: serverStreamEndedResolveP } = 36 | utils.promise(); 37 | quicServer.addEventListener( 38 | events.EventQUICServerConnection.name, 39 | (evt: events.EventQUICServerConnection) => { 40 | const connection = evt.detail; 41 | connection.addEventListener( 42 | events.EventQUICConnectionStream.name, 43 | async (evt: events.EventQUICConnectionStream) => { 44 | const stream = evt.detail; 45 | await stream.writable.close(); 46 | // Consume until graceful close of readable 47 | for await (const _ of stream.readable) { 48 | // Do nothing, only consume 49 | } 50 | serverStreamEndedResolveP(); 51 | }, 52 | ); 53 | }, 54 | ); 55 | await quicServer.start(); 56 | const quicClient = await QUICClient.createQUICClient({ 57 | host: utils.resolvesZeroIP(quicServer.host), 58 | port: quicServer.port, 59 | config: { 60 | verifyPeer: false, 61 | }, 62 | crypto: { 63 | ops: { 64 | randomBytes: testsUtils.randomBytes, 65 | }, 66 | }, 67 | logger: logger.getChild('QUICClient'), 68 | }); 69 | const clientStream = quicClient.connection.newStream(); 70 | const writer = clientStream.writable.getWriter(); 71 | await writer.write(data1KiB); 72 | for await (const _ of clientStream.readable) { 73 | // No nothing, just consume 74 | } 75 | const summary = await b.suite( 76 | path.basename(filePath, path.extname(filePath)), 77 | b.add('send 1Kib of data over QUICStream with UDP socket', async () => { 78 | await writer.write(data1KiB); 79 | }), 80 | ...suiteCommon, 81 | ); 82 | await writer.close(); 83 | await serverStreamEndedP; 84 | await quicClient?.destroy(); 85 | await quicServer?.stop(); 86 | return summary; 87 | } 88 | 89 | if (import.meta.url.startsWith('file:')) { 90 | const modulePath = url.fileURLToPath(import.meta.url); 91 | if (process.argv[1] === modulePath) { 92 | void main(); 93 | } 94 | } 95 | 96 | export default main; 97 | -------------------------------------------------------------------------------- /benches/stream_1KiB_FFI.ts: -------------------------------------------------------------------------------- 1 | import type { Host, Port } from '#types.js'; 2 | import type { Connection } from '#native/types.js'; 3 | import url from 'node:url'; 4 | import path from 'node:path'; 5 | import b from 'benny'; 6 | import { suiteCommon } from './utils/utils.js'; 7 | import * as testsUtils from '../tests/utils.js'; 8 | import * as utils from '#utils.js'; 9 | import quiche from '#native/quiche.js'; 10 | import { buildQuicheConfig, clientDefault, serverDefault } from '#config.js'; 11 | import QUICConnectionId from '#QUICConnectionId.js'; 12 | 13 | const filePath = url.fileURLToPath(import.meta.url); 14 | 15 | async function main() { 16 | const data1KiB = Buffer.allocUnsafe(1024); 17 | const dataBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); 18 | 19 | function sendPacket( 20 | connectionSource: Connection, 21 | connectionDestination: Connection, 22 | ): boolean { 23 | const result = connectionSource.send(dataBuffer); 24 | if (result === null) return false; 25 | const [serverSendLength, sendInfo] = result; 26 | connectionDestination.recv(dataBuffer.subarray(0, serverSendLength), { 27 | to: sendInfo.to, 28 | from: sendInfo.from, 29 | }); 30 | return true; 31 | } 32 | 33 | const localHost = '127.0.0.1' as Host; 34 | const clientHost = { 35 | host: localHost, 36 | port: 55555 as Port, 37 | }; 38 | const serverHost = { 39 | host: localHost, 40 | port: 55556, 41 | }; 42 | const crypto = { 43 | key: await testsUtils.generateKeyHMAC(), 44 | ops: { 45 | sign: testsUtils.signHMAC, 46 | verify: testsUtils.verifyHMAC, 47 | randomBytes: testsUtils.randomBytes, 48 | }, 49 | }; 50 | 51 | // Setting up connection state 52 | const clientConfig = buildQuicheConfig({ 53 | ...clientDefault, 54 | verifyPeer: false, 55 | }); 56 | 57 | const tlsConfigServer = await testsUtils.generateTLSConfig('RSA'); 58 | const serverConfig = buildQuicheConfig({ 59 | ...serverDefault, 60 | 61 | key: tlsConfigServer.leafKeyPairPEM.privateKey, 62 | cert: tlsConfigServer.leafCertPEM, 63 | }); 64 | 65 | // Randomly generate the client SCID 66 | const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); 67 | await crypto.ops.randomBytes(scidBuffer); 68 | const clientScid = new QUICConnectionId(scidBuffer); 69 | const clientConn = quiche.Connection.connect( 70 | null, 71 | clientScid, 72 | clientHost, 73 | serverHost, 74 | clientConfig, 75 | ); 76 | 77 | const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); 78 | const sendResult = clientConn.send(clientBuffer); 79 | if (sendResult === null) throw Error('unexpected send fail'); 80 | let [clientSendLength] = sendResult; 81 | const clientHeaderInitial = quiche.Header.fromSlice( 82 | clientBuffer.subarray(0, clientSendLength), 83 | quiche.MAX_CONN_ID_LEN, 84 | ); 85 | const clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); 86 | 87 | // Derives a new SCID by signing the client's generated DCID 88 | // This is only used during the stateless retry 89 | const serverScid = new QUICConnectionId( 90 | await crypto.ops.sign(crypto.key, clientDcid), 91 | 0, 92 | quiche.MAX_CONN_ID_LEN, 93 | ); 94 | // Stateless retry 95 | const token = await utils.mintToken(clientDcid, clientHost.host, crypto); 96 | const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); 97 | const retryDatagramLength = quiche.retry( 98 | clientScid, 99 | clientDcid, 100 | serverScid, 101 | token, 102 | clientHeaderInitial.version, 103 | retryDatagram, 104 | ); 105 | 106 | // Retry gets sent back to be processed by the client 107 | clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { 108 | to: clientHost, 109 | from: serverHost, 110 | }); 111 | 112 | // Client will retry the initial packet with the token 113 | const sendResult2 = clientConn.send(clientBuffer); 114 | if (sendResult2 === null) throw Error('Unexpected send fail'); 115 | [clientSendLength] = sendResult2; 116 | 117 | // Server accept 118 | const serverConn = quiche.Connection.accept( 119 | serverScid, 120 | clientDcid, 121 | serverHost, 122 | clientHost, 123 | serverConfig, 124 | ); 125 | // Server receives the retried initial frame 126 | serverConn.recv(clientBuffer.subarray(0, clientSendLength), { 127 | to: serverHost, 128 | from: clientHost, 129 | }); 130 | 131 | // Client <-initial- server 132 | sendPacket(serverConn, clientConn); 133 | // Client -initial-> server 134 | sendPacket(clientConn, serverConn); 135 | // Client <-handshake- server 136 | sendPacket(serverConn, clientConn); 137 | // Client -handshake-> server 138 | sendPacket(clientConn, serverConn); 139 | // Client <-short- server 140 | sendPacket(serverConn, clientConn); 141 | // Client -short-> server 142 | sendPacket(clientConn, serverConn); 143 | // Both are established 144 | 145 | // Setting up runtimes 146 | 147 | // Resolved when client receives data 148 | let clientWaitRecvProm = utils.promise(); 149 | let serverWaitRecvProm = utils.promise(); 150 | const clientBuf = Buffer.allocUnsafe(1024); 151 | 152 | const clientSend = () => { 153 | let sent = false; 154 | while (true) { 155 | if (sendPacket(clientConn, serverConn)) { 156 | sent = true; 157 | } else { 158 | break; 159 | } 160 | } 161 | if (sent) serverWaitRecvProm.resolveP(); 162 | }; 163 | const clientWrite = (buffer: Buffer) => { 164 | // Write buffer to stream 165 | clientConn.streamSend(0, buffer, false); 166 | // Trigger send 167 | clientSend(); 168 | }; 169 | const clientRuntime = (async () => { 170 | while (true) { 171 | await clientWaitRecvProm.p; 172 | clientWaitRecvProm = utils.promise(); 173 | // Process streams. 174 | for (const streamId of serverConn.readable()) { 175 | while (true) { 176 | // Read and ditch information 177 | if (clientConn.streamRecv(streamId, clientBuf) === null) break; 178 | } 179 | } 180 | // Process sends. 181 | clientSend(); 182 | // Check state change, 183 | if (clientConn.isClosed() || clientConn.isDraining()) break; 184 | } 185 | })(); 186 | 187 | const serverSend = () => { 188 | let sent = false; 189 | while (true) { 190 | if (sendPacket(serverConn, clientConn)) { 191 | sent = true; 192 | } else { 193 | break; 194 | } 195 | } 196 | if (sent) clientWaitRecvProm.resolveP(); 197 | }; 198 | 199 | const serverRuntime = (async () => { 200 | while (true) { 201 | await serverWaitRecvProm.p; 202 | serverWaitRecvProm = utils.promise(); 203 | // Process streams. 204 | for (const streamId of serverConn.readable()) { 205 | while (true) { 206 | // Read and ditch information 207 | if (serverConn.streamRecv(streamId, clientBuf) === null) break; 208 | } 209 | } 210 | // Process sends. 211 | serverSend(); 212 | // Check state change, 213 | if (serverConn.isClosed() || serverConn.isDraining()) break; 214 | } 215 | })(); 216 | 217 | const summary = await b.suite( 218 | path.basename(filePath, path.extname(filePath)), 219 | b.add('send 1Kib of data over quiche FFI with no UDP socket', async () => { 220 | clientWrite(data1KiB); 221 | }), 222 | ...suiteCommon, 223 | ); 224 | 225 | clientConn.close(true, 0, Buffer.from([])); 226 | serverConn.close(true, 0, Buffer.from([])); 227 | clientSend(); 228 | serverSend(); 229 | await Promise.all([clientRuntime, serverRuntime]); 230 | 231 | return summary; 232 | } 233 | 234 | if (import.meta.url.startsWith('file:')) { 235 | const modulePath = url.fileURLToPath(import.meta.url); 236 | if (process.argv[1] === modulePath) { 237 | void main(); 238 | } 239 | } 240 | 241 | export default main; 242 | -------------------------------------------------------------------------------- /benches/utils/BenchHandler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '@matrixai/logger'; 2 | 3 | /** 4 | * Custom bench handler where emission is free 5 | * This avoids benchmarking stdout/stderr 6 | * which is dependent on the operating system 7 | * and whether it is TTY or a pipe or a file 8 | */ 9 | class BenchHandler extends Handler { 10 | public emit(): void { 11 | // This is a noop 12 | } 13 | } 14 | 15 | export default BenchHandler; 16 | -------------------------------------------------------------------------------- /benches/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BenchHandler } from './BenchHandler.js'; 2 | export * from './utils.js'; 3 | -------------------------------------------------------------------------------- /benches/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import url from 'node:url'; 4 | import b from 'benny'; 5 | import { codeBlock } from 'common-tags'; 6 | import packageJson from '../../package.json'; 7 | 8 | const benchesPath = path.dirname( 9 | path.dirname(url.fileURLToPath(import.meta.url)), 10 | ); 11 | 12 | const suiteCommon = [ 13 | b.cycle(), 14 | b.complete(), 15 | b.save({ 16 | file: (summary) => summary.name, 17 | folder: path.join(benchesPath, 'results'), 18 | version: packageJson.version, 19 | details: true, 20 | }), 21 | b.save({ 22 | file: (summary) => summary.name, 23 | folder: path.join(benchesPath, 'results'), 24 | version: packageJson.version, 25 | format: 'chart.html', 26 | }), 27 | b.complete((summary) => { 28 | const filePath = path.join( 29 | benchesPath, 30 | 'results', 31 | summary.name + '_metrics.txt', 32 | ); 33 | fs.writeFileSync( 34 | filePath, 35 | codeBlock` 36 | # TYPE ${summary.name}_ops gauge 37 | ${summary.results 38 | .map( 39 | (result) => 40 | `${summary.name}_ops{name="${result.name}"} ${result.ops}`, 41 | ) 42 | .join('\n')} 43 | 44 | # TYPE ${summary.name}_margin gauge 45 | ${summary.results 46 | .map( 47 | (result) => 48 | `${summary.name}_margin{name="${result.name}"} ${result.margin}`, 49 | ) 50 | .join('\n')} 51 | 52 | # TYPE ${summary.name}_samples counter 53 | ${summary.results 54 | .map( 55 | (result) => 56 | `${summary.name}_samples{name="${result.name}"} ${result.samples}`, 57 | ) 58 | .join('\n')} 59 | ` + '\n', 60 | ); 61 | // eslint-disable-next-line no-console 62 | console.log('\nSaved to:', path.resolve(filePath)); 63 | }), 64 | ]; 65 | 66 | export { benchesPath, suiteCommon }; 67 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate napi_build; 2 | 3 | fn main() { 4 | napi_build::setup(); 5 | } 6 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #098658; 13 | --dark-hl-5: #B5CEA8; 14 | --light-hl-6: #EE0000; 15 | --dark-hl-6: #D7BA7D; 16 | --light-hl-7: #267F99; 17 | --dark-hl-7: #4EC9B0; 18 | --light-hl-8: #001080; 19 | --dark-hl-8: #9CDCFE; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /examples/test_example.rs: -------------------------------------------------------------------------------- 1 | use std::*; 2 | use std::net::*; 3 | 4 | const MAX_DATAGRAM_SIZE: usize = 1350; 5 | 6 | // FIXME: this is a temp test file, need to remove before merging 7 | 8 | /// Generate a stateless retry token. 9 | /// 10 | /// The token includes the static string `"quiche"` followed by the IP address 11 | /// of the client and by the original destination connection ID generated by the 12 | /// client. 13 | /// 14 | /// Note that this function is only an example and doesn't do any cryptographic 15 | /// authenticate of the token. *It should not be used in production system*. 16 | fn mint_token(hdr: &quiche::Header, src: &net::SocketAddr) -> Vec { 17 | let mut token = Vec::new(); 18 | 19 | token.extend_from_slice(b"quiche"); 20 | 21 | let addr = match src.ip() { 22 | std::net::IpAddr::V4(a) => a.octets().to_vec(), 23 | std::net::IpAddr::V6(a) => a.octets().to_vec(), 24 | }; 25 | 26 | token.extend_from_slice(&addr); 27 | token.extend_from_slice(&hdr.dcid); 28 | 29 | token 30 | } 31 | 32 | fn main() { 33 | 34 | const LOOPS: i32 = 200_000; 35 | let mut buf = [0; 65535]; 36 | let mut out = [0; MAX_DATAGRAM_SIZE]; 37 | 38 | // Create the configuration for the QUIC connection. 39 | let mut client_config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap(); 40 | 41 | // *CAUTION*: this should not be set to `false` in production!!! 42 | client_config.verify_peer(false); 43 | 44 | client_config 45 | .set_application_protos(&[ 46 | b"hq-interop", 47 | b"hq-29", 48 | b"hq-28", 49 | b"hq-27", 50 | b"http/0.9", 51 | ]) 52 | .unwrap(); 53 | 54 | client_config.set_max_idle_timeout(5000); 55 | client_config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE); 56 | client_config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE); 57 | client_config.set_initial_max_data(10_000_000); 58 | client_config.set_initial_max_stream_data_bidi_local(1_000_000); 59 | client_config.set_initial_max_stream_data_bidi_remote(1_000_000); 60 | client_config.set_initial_max_streams_bidi(100); 61 | client_config.set_initial_max_streams_uni(100); 62 | client_config.set_disable_active_migration(true); 63 | 64 | // server config 65 | // Create the configuration for the QUIC connections. 66 | let mut server_config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap(); 67 | 68 | server_config 69 | .load_cert_chain_from_pem_file("./examples/cert.crt") 70 | .unwrap(); 71 | server_config 72 | .load_priv_key_from_pem_file("./examples/cert.key") 73 | .unwrap(); 74 | 75 | server_config 76 | .set_application_protos(&[ 77 | b"hq-interop", 78 | b"hq-29", 79 | b"hq-28", 80 | b"hq-27", 81 | b"http/0.9", 82 | ]) 83 | .unwrap(); 84 | 85 | server_config.set_max_idle_timeout(5000); 86 | server_config.set_max_recv_udp_payload_size(MAX_DATAGRAM_SIZE); 87 | server_config.set_max_send_udp_payload_size(MAX_DATAGRAM_SIZE); 88 | server_config.set_initial_max_data(10_000_000); 89 | server_config.set_initial_max_stream_data_bidi_local(1_000_000); 90 | server_config.set_initial_max_stream_data_bidi_remote(1_000_000); 91 | server_config.set_initial_max_stream_data_uni(1_000_000); 92 | server_config.set_initial_max_streams_bidi(100); 93 | server_config.set_initial_max_streams_uni(100); 94 | server_config.set_disable_active_migration(true); 95 | server_config.enable_early_data(); 96 | 97 | 98 | 99 | // Generate a random source connection ID for the connection. 100 | let scid = [1; quiche::MAX_CONN_ID_LEN]; 101 | let scid = quiche::ConnectionId::from_ref(&scid); 102 | 103 | // Get local address. 104 | let client_addr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 55555); 105 | let server_addr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 55556); 106 | 107 | let forward_info = quiche::RecvInfo { 108 | from: client_addr, 109 | to: server_addr, 110 | }; 111 | let reverse_info = quiche::RecvInfo { 112 | from: server_addr, 113 | to: client_addr, 114 | }; 115 | 116 | // Create a QUIC connection and initiate handshake. 117 | let mut client_conn = 118 | quiche::connect(None, &scid, client_addr, server_addr, &mut client_config) 119 | .unwrap(); 120 | 121 | let (write, send_info) = client_conn.send(&mut out).expect("initial send failed"); 122 | 123 | // Parse the QUIC packet's header. 124 | let hdr = match quiche::Header::from_slice( 125 | &mut out[..write], 126 | quiche::MAX_CONN_ID_LEN, 127 | ) { 128 | Ok(v) => v, 129 | 130 | Err(e) => { 131 | panic!("Parsing packet header failed: {:?}", e); 132 | }, 133 | }; 134 | 135 | 136 | let server_conn_id = [0; quiche::MAX_CONN_ID_LEN]; 137 | 138 | let mut scid: [u8; quiche::MAX_CONN_ID_LEN] = [0; quiche::MAX_CONN_ID_LEN]; 139 | scid.copy_from_slice(&server_conn_id); 140 | let scid = quiche::ConnectionId::from_ref(&scid); 141 | 142 | // Do stateless retry if the client didn't send a token. 143 | let new_token = mint_token(&hdr, &send_info.from); 144 | 145 | let len = quiche::retry( 146 | &hdr.scid, 147 | &hdr.dcid, 148 | &scid, 149 | &new_token, 150 | hdr.version, 151 | &mut out, 152 | ) 153 | .unwrap(); 154 | 155 | client_conn.recv(&mut out[..len], reverse_info).expect("something"); 156 | let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); 157 | 158 | let mut server_conn = quiche::accept( 159 | &scid, 160 | Some(&hdr.dcid), 161 | server_addr, 162 | client_addr, 163 | &mut server_config, 164 | ) 165 | .unwrap(); 166 | 167 | let _read = match server_conn.recv(&mut out[..write], forward_info) { 168 | Ok(v) => v, 169 | 170 | Err(e) => { 171 | panic!("{} recv failed: {:?}", server_conn.trace_id(), e); 172 | }, 173 | }; 174 | 175 | // Client <-initial- server 176 | let (write, _send_info) = server_conn.send(&mut out).expect("initial send failed"); 177 | let _read = match client_conn.recv(&mut out[..write], reverse_info) { 178 | Ok(v) => v, 179 | Err(e) => { 180 | panic!("{} recv failed: {:?}", client_conn.trace_id(), e); 181 | }, 182 | }; 183 | 184 | // Client -initial-> server 185 | let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); 186 | let _read = match server_conn.recv(&mut out[..write], forward_info) { 187 | Ok(v) => v, 188 | Err(e) => { 189 | panic!("{} recv failed: {:?}", server_conn.trace_id(), e); 190 | }, 191 | }; 192 | 193 | // Client <-handshake- server 194 | let (write, _send_info) = server_conn.send(&mut out).expect("initial send failed"); 195 | let _read = match client_conn.recv(&mut out[..write], reverse_info) { 196 | Ok(v) => v, 197 | Err(e) => { 198 | panic!("{} recv failed: {:?}", client_conn.trace_id(), e); 199 | }, 200 | }; 201 | 202 | // Client -handshake-> server 203 | let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); 204 | let _read = match server_conn.recv(&mut out[..write], forward_info) { 205 | Ok(v) => v, 206 | Err(e) => { 207 | panic!("{} recv failed: {:?}", server_conn.trace_id(), e); 208 | }, 209 | }; 210 | 211 | // Client <-short- server 212 | let (write, _send_info) = server_conn.send(&mut out).expect("initial send failed"); 213 | let _read = match client_conn.recv(&mut out[..write], reverse_info) { 214 | Ok(v) => v, 215 | Err(e) => { 216 | panic!("{} recv failed: {:?}", client_conn.trace_id(), e); 217 | }, 218 | }; 219 | 220 | // Client -short-> server 221 | let (write, _send_info) = client_conn.send(&mut out).expect("initial send failed"); 222 | let _read = match server_conn.recv(&mut out[..write], forward_info) { 223 | Ok(v) => v, 224 | Err(e) => { 225 | panic!("{} recv failed: {:?}", server_conn.trace_id(), e); 226 | }, 227 | }; 228 | // Both are established 229 | 230 | // main loop 231 | let message = [0; 1024]; 232 | // let before = time::Instant::now(); 233 | // let mut send_time= 0; 234 | // let mut client_send_time= 0; 235 | // let mut server_recv_time= 0; 236 | // let mut stream_time= 0; 237 | // let mut server_send_time= 0; 238 | // let mut client_recv_time= 0; 239 | for _n in 1..LOOPS { 240 | // send data 241 | // let now = time::Instant::now(); 242 | client_conn.stream_send(0, &message, false).expect("Stream send failed"); 243 | // send_time += now.elapsed().as_nanos(); 244 | // sending forward packets 245 | loop { 246 | // let now = time::Instant::now(); 247 | let (write, _send_info) = match client_conn.send(&mut out){ 248 | Ok(v) => v, 249 | Err(quiche::Error::Done) => { 250 | break; 251 | }, 252 | Err(e) => { 253 | panic!("{} recv failed: {:?}", client_conn.trace_id(), e); 254 | }, 255 | }; 256 | // client_send_time += now.elapsed().as_nanos(); 257 | // let now = time::Instant::now(); 258 | let _read = match server_conn.recv(&mut out[..write], forward_info) { 259 | Ok(v) => v, 260 | Err(e) => { 261 | panic!("{} recv failed: {:?}", server_conn.trace_id(), e); 262 | }, 263 | }; 264 | // server_recv_time += now.elapsed().as_nanos(); 265 | }; 266 | // Processing streams 267 | // let now = time::Instant::now(); 268 | for s in server_conn.readable() { 269 | while let Ok(..) = 270 | server_conn.stream_recv(s, &mut buf) 271 | { 272 | // Do nothing 273 | } 274 | } 275 | // stream_time += now.elapsed().as_nanos(); 276 | // Processing reverse packets 277 | 'reverse: loop { 278 | // let now = time::Instant::now(); 279 | let (write, _send_info) = match server_conn.send(&mut out) { 280 | Ok(v) => v, 281 | Err(quiche::Error::Done) => { 282 | break 'reverse; 283 | }, 284 | Err(e) => { 285 | panic!("{} recv failed: {:?}", server_conn.trace_id(), e); 286 | }, 287 | }; 288 | // server_send_time += now.elapsed().as_nanos(); 289 | // let now = time::Instant::now(); 290 | let _read = match client_conn.recv(&mut out[..write], reverse_info) { 291 | Ok(v) => v, 292 | Err(e) => { 293 | panic!("{} recv failed: {:?}", client_conn.trace_id(), e); 294 | }, 295 | }; 296 | // client_recv_time += now.elapsed().as_nanos(); 297 | }; 298 | } 299 | // let total_time = send_time + server_send_time + client_recv_time + stream_time + client_send_time + client_recv_time; 300 | // println!("total time elapsed {}ms", before.elapsed().as_millis()); 301 | // println!("sum times {}ms", total_time / (1000*1000) ); 302 | // println!("send time {}ms {}%", send_time / (1000*1000), send_time * 100 / total_time); 303 | // println!("server send time {}ms {}%", server_send_time / (1000*1000), server_send_time * 100 / total_time); 304 | // println!("client recv time {}ms {}%", client_recv_time / (1000*1000), client_recv_time * 100 / total_time); 305 | // println!("stream proc time {}ms {}%", stream_time / (1000*1000), stream_time * 100 / total_time); 306 | // println!("client send time {}ms {}%", client_send_time / (1000*1000), client_send_time * 100 / total_time); 307 | // println!("server recv time {}ms {}%", server_recv_time / (1000*1000), server_recv_time * 100 / total_time); 308 | } 309 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1716357214, 24 | "narHash": "sha256-gQh7A8QOJLUhO7bdtQ8ZW9/KM70ciKskxSYgC1Lzm6g=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "e69e710edfed397959507bcee120ec8a9c7ff03e", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "repo": "nixpkgs", 33 | "rev": "e69e710edfed397959507bcee120ec8a9c7ff03e", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-matrix": { 38 | "inputs": { 39 | "nixpkgs": "nixpkgs" 40 | }, 41 | "locked": { 42 | "lastModified": 1736140072, 43 | "narHash": "sha256-MgtcAA+xPldS0WlV16TjJ0qgFzGvKuGM9p+nPUxpUoA=", 44 | "owner": "MatrixAI", 45 | "repo": "nixpkgs-matrix", 46 | "rev": "029084026bc4a35bce81bac898aa695f41993e18", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "id": "nixpkgs-matrix", 51 | "type": "indirect" 52 | } 53 | }, 54 | "root": { 55 | "inputs": { 56 | "flake-utils": "flake-utils", 57 | "nixpkgs-matrix": "nixpkgs-matrix" 58 | } 59 | }, 60 | "systems": { 61 | "locked": { 62 | "lastModified": 1681028828, 63 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 64 | "owner": "nix-systems", 65 | "repo": "default", 66 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nix-systems", 71 | "repo": "default", 72 | "type": "github" 73 | } 74 | } 75 | }, 76 | "root": "root", 77 | "version": 7 78 | } 79 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs-matrix = { 4 | type = "indirect"; 5 | id = "nixpkgs-matrix"; 6 | inputs.nixpkgs.url = 7 | "github:NixOS/nixpkgs?rev=e69e710edfed397959507bcee120ec8a9c7ff03e"; 8 | }; 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | }; 11 | 12 | outputs = { nixpkgs-matrix, flake-utils, ... }: 13 | flake-utils.lib.eachDefaultSystem (system: 14 | let 15 | pkgs = nixpkgs-matrix.legacyPackages.${system}; 16 | shell = { ci ? false }: 17 | with pkgs; 18 | mkShell { 19 | nativeBuildInputs = [ 20 | nodejs_20 21 | shellcheck 22 | gitAndTools.gh 23 | rustc 24 | cargo 25 | cmake 26 | rustPlatform.bindgenHook 27 | ]; 28 | NIX_DONT_SET_RPATH = true; 29 | NIX_NO_SELF_RPATH = true; 30 | RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; 31 | shellHook = '' 32 | echo "Entering $(npm pkg get name)" 33 | set -o allexport 34 | . <(polykey secrets env js-quic) 35 | set +o allexport 36 | set -v 37 | ${lib.optionalString ci '' 38 | set -o errexit 39 | set -o nounset 40 | set -o pipefail 41 | shopt -s inherit_errexit 42 | ''} 43 | mkdir --parents "$(pwd)/tmp" 44 | export PATH="$(pwd)/dist/bin:$(npm root)/.bin:$PATH" 45 | npm install --ignore-scripts 46 | set +v 47 | ''; 48 | }; 49 | in { 50 | devShells = { 51 | default = shell { ci = false; }; 52 | ci = shell { ci = true; }; 53 | }; 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import url from 'node:url'; 3 | import tsconfigJSON from './tsconfig.json' assert { type: "json" }; 4 | 5 | const projectPath = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | // Global variables that are shared across the jest worker pool 8 | // These variables must be static and serializable 9 | const globals = { 10 | // Absolute directory to the project root 11 | projectDir: projectPath, 12 | // Absolute directory to the test root 13 | testDir: path.join(projectPath, 'tests'), 14 | // Default asynchronous test timeout 15 | defaultTimeout: 20000, 16 | // Timeouts rely on setTimeout which takes 32 bit numbers 17 | maxTimeout: Math.pow(2, 31) - 1, 18 | }; 19 | 20 | // The `globalSetup` and `globalTeardown` cannot access the `globals` 21 | // They run in their own process context 22 | // They can however receive the process environment 23 | // Use `process.env` to set variables 24 | 25 | const config = { 26 | testEnvironment: 'node', 27 | verbose: true, 28 | collectCoverage: false, 29 | cacheDirectory: '/tmp/jest', 30 | coverageDirectory: '/tmp/coverage', 31 | roots: ['/tests'], 32 | testMatch: ['**/?(*.)+(spec|test|unit.test).+(ts|tsx|js|jsx)'], 33 | transform: { 34 | "^.+\\.(t|j)sx?$": [ 35 | "@swc/jest", 36 | { 37 | jsc: { 38 | parser: { 39 | syntax: "typescript", 40 | tsx: true, 41 | decorators: tsconfigJSON.compilerOptions.experimentalDecorators, 42 | dynamicImport: true, 43 | }, 44 | target: tsconfigJSON.compilerOptions.target.toLowerCase(), 45 | keepClassNames: true, 46 | }, 47 | } 48 | ], 49 | }, 50 | reporters: [ 51 | 'default', 52 | ['jest-junit', { 53 | outputDirectory: '/tmp/junit', 54 | classNameTemplate: '{classname}', 55 | ancestorSeparator: ' > ', 56 | titleTemplate: '{title}', 57 | addFileAttribute: 'true', 58 | reportTestSuiteErrors: 'true', 59 | }], 60 | ], 61 | collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts'], 62 | coverageReporters: ['text', 'cobertura'], 63 | globals, 64 | // Global setup script executed once before all test files 65 | globalSetup: '/tests/globalSetup.ts', 66 | // Global teardown script executed once after all test files 67 | globalTeardown: '/tests/globalTeardown.ts', 68 | // Setup files are executed before each test file 69 | // Can access globals 70 | setupFiles: ['/tests/setup.ts'], 71 | // Setup files after env are executed before each test file 72 | // after the jest test environment is installed 73 | // Can access globals 74 | setupFilesAfterEnv: [ 75 | 'jest-extended/all', 76 | '/tests/setupAfterEnv.ts' 77 | ], 78 | moduleNameMapper: { 79 | "^(\\.{1,2}/.*)\\.js$": "$1", 80 | }, 81 | extensionsToTreatAsEsm: ['.ts', '.tsx', '.mts'], 82 | }; 83 | 84 | export default config; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrixai/quic", 3 | "version": "2.0.9", 4 | "author": "Matrix AI", 5 | "contributors": [ 6 | { 7 | "name": "Roger Qiu" 8 | }, 9 | { 10 | "name": "Brian Botha" 11 | } 12 | ], 13 | "description": "QUIC", 14 | "license": "Apache-2.0", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/MatrixAI/js-quic.git" 18 | }, 19 | "type": "module", 20 | "exports": { 21 | "./package.json": "./package.json", 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "import": "./dist/index.js" 25 | }, 26 | "./*.js": { 27 | "types": "./dist/*.d.ts", 28 | "import": "./dist/*.js" 29 | }, 30 | "./*": "./dist/*" 31 | }, 32 | "imports": { 33 | "#*": "./dist/*" 34 | }, 35 | "napi": { 36 | "name": "quic" 37 | }, 38 | "scripts": { 39 | "prepare": "tsc -p ./tsconfig.build.json", 40 | "prebuild": "node ./scripts/prebuild.mjs", 41 | "build": "shx rm -rf ./dist && tsc -p ./tsconfig.build.json", 42 | "version": "node ./scripts/version.mjs", 43 | "prepublishOnly": "node ./scripts/prepublishOnly.mjs", 44 | "tsx": "tsx", 45 | "test": "node ./scripts/test.mjs", 46 | "lint": "matrixai-lint", 47 | "lintfix": "matrixai-lint --fix", 48 | "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", 49 | "bench": "tsc -p ./tsconfig.build.json && shx rm -rf ./benches/results && tsx ./benches/index.ts" 50 | }, 51 | "dependencies": { 52 | "@matrixai/async-cancellable": "^2.0.1", 53 | "@matrixai/async-init": "^2.1.2", 54 | "@matrixai/async-locks": "^5.0.2", 55 | "@matrixai/contexts": "^2.0.2", 56 | "@matrixai/errors": "^2.1.3", 57 | "@matrixai/events": "^4.0.1", 58 | "@matrixai/logger": "^4.0.3", 59 | "@matrixai/resources": "^2.0.1", 60 | "@matrixai/timer": "^2.1.1", 61 | "ip-num": "^1.5.0" 62 | }, 63 | "optionalDependencies": { 64 | "@matrixai/quic-darwin-arm64": "2.0.9", 65 | "@matrixai/quic-darwin-universal": "2.0.9", 66 | "@matrixai/quic-darwin-x64": "2.0.9", 67 | "@matrixai/quic-linux-x64": "2.0.9", 68 | "@matrixai/quic-win32-x64": "2.0.9" 69 | }, 70 | "devDependencies": { 71 | "@matrixai/lint": "^0.2.11", 72 | "@fast-check/jest": "^2.1.0", 73 | "@napi-rs/cli": "^2.15.2", 74 | "@noble/ed25519": "^1.7.3", 75 | "@peculiar/asn1-pkcs8": "^2.3.0", 76 | "@peculiar/asn1-schema": "^2.3.0", 77 | "@peculiar/asn1-x509": "^2.3.0", 78 | "@peculiar/webcrypto": "^1.4.3", 79 | "@peculiar/x509": "^1.8.3", 80 | "@swc/core": "1.3.82", 81 | "@swc/jest": "^0.2.29", 82 | "@types/jest": "^29.5.2", 83 | "@types/node": "^20.5.7", 84 | "benny": "^3.7.1", 85 | "common-tags": "^1.8.2", 86 | "fast-check": "^4.0.0", 87 | "jest": "^29.6.2", 88 | "jest-extended": "^4.0.0", 89 | "jest-junit": "^16.0.0", 90 | "semver": "^7.3.7", 91 | "shx": "^0.3.4", 92 | "sodium-native": "^3.4.1", 93 | "systeminformation": "^5.18.5", 94 | "ts-node": "^10.9.1", 95 | "tsx": "^3.12.7", 96 | "typedoc": "^0.24.8", 97 | "typescript": "^5.1.6" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /scripts/brew-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit # abort on nonzero exitstatus 4 | set -o nounset # abort on unbound variable 5 | set -o pipefail # don't hide errors within pipes 6 | 7 | export HOMEBREW_NO_INSTALL_UPGRADE=1 8 | export HOMEBREW_NO_INSTALL_CLEANUP=1 9 | export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 10 | export HOMEBREW_NO_AUTO_UPDATE=1 11 | export HOMEBREW_NO_ANALYTICS=1 12 | 13 | brew reinstall node@20 14 | brew link --overwrite node@20 15 | brew install cmake 16 | brew link --overwrite cmake 17 | brew install rustup-init 18 | brew link --overwrite rustup-init 19 | 20 | # Brew does not provide specific versions of rust 21 | # However rustup provides specific versions 22 | # Here we provide both toolchains 23 | echo "Running rustup-init" 24 | rustup-init --default-toolchain 1.68.2 -y 25 | echo "Adding x86_64-apple-darwin as target" 26 | rustup target add x86_64-apple-darwin 27 | echo "Adding aarch64-apple-darwin as target" 28 | rustup target add aarch64-apple-darwin 29 | echo "Completed brew setup" 30 | -------------------------------------------------------------------------------- /scripts/choco-install.ps1: -------------------------------------------------------------------------------- 1 | function Save-ChocoPackage { 2 | param ( 3 | $PackageName 4 | ) 5 | Rename-Item -Path "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nupkg" -NewName "$PackageName.nupkg.zip" -ErrorAction:SilentlyContinue 6 | Expand-Archive -LiteralPath "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nupkg.zip" -DestinationPath "$env:ChocolateyInstall\lib\$PackageName" -Force 7 | Remove-Item "$env:ChocolateyInstall\lib\$PackageName\_rels" -Recurse 8 | Remove-Item "$env:ChocolateyInstall\lib\$PackageName\package" -Recurse 9 | Remove-Item "$env:ChocolateyInstall\lib\$PackageName\[Content_Types].xml" 10 | New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey\$PackageName" -ItemType "directory" -ErrorAction:SilentlyContinue 11 | choco pack "$env:ChocolateyInstall\lib\$PackageName\$PackageName.nuspec" --outdir "${PSScriptRoot}\..\tmp\chocolatey\$PackageName" --no-progress 12 | } 13 | 14 | # Check for existence of required environment variables 15 | if ( $null -eq $env:ChocolateyInstall ) { 16 | [Console]::Error.WriteLine('Missing $env:ChocolateyInstall environment variable') 17 | exit 1 18 | } 19 | 20 | # Add the cached packages with source priority 1 (Chocolatey community is 0) 21 | New-Item -Path "${PSScriptRoot}\..\tmp\chocolatey" -ItemType "directory" -ErrorAction:SilentlyContinue 22 | choco source add --name="cache" --source="${PSScriptRoot}\..\tmp\chocolatey" --priority=1 --no-progress 23 | 24 | # Install nodejs v20.5.1 (will use cache if exists) 25 | $nodejs = "nodejs" 26 | choco install "$nodejs" --version="20.5.1" --require-checksums -y --no-progress 27 | # Internalise nodejs to cache if doesn't exist 28 | if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$nodejs\$nodejs.20.5.1.nupkg" -PathType Leaf) ) { 29 | Save-ChocoPackage -PackageName $nodejs 30 | } 31 | 32 | # Install rust v1.68.0 (will use cache if exists) 33 | $rust = "rust-ms" 34 | choco install "$rust" --version="1.68.0" --require-checksums -y --no-progress 35 | # Internalise rust to cache if doesn't exist 36 | if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$rust\$rust.1.68.0.nupkg" -PathType Leaf) ) { 37 | Save-ChocoPackage -PackageName $rust 38 | } 39 | 40 | # Install llvm v16.0.3 (will use cache if exists) 41 | $llvm = "llvm" 42 | choco install "$llvm" --version="16.0.3" --require-checksums -y --no-progress 43 | # Internalise rust to cache if doesn't exist 44 | if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$llvm\$llvm.16.0.3.nupkg" -PathType Leaf) ) { 45 | Save-ChocoPackage -PackageName $llvm 46 | } 47 | 48 | # Install nasm v2.16.01.20221231 (will use cache if exists) 49 | $nasm = "nasm" 50 | choco install "$nasm" --version="2.16.01.20221231" --require-checksums -y --no-progress 51 | # Internalise rust to cache if doesn't exist 52 | if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$nasm\$nasm.2.16.01.20221231.nupkg" -PathType Leaf) ) { 53 | Save-ChocoPackage -PackageName $nasm 54 | } 55 | 56 | # Install Windows SDK v10.0.22621.2 (will use cache if exists) 57 | $windowsSdk = "windows-sdk-11-version-22h2-all" 58 | choco install $windowsSdk --version="10.0.22621.2" --require-checksums -y --no-progress 59 | # Internalise rust to cache if doesn't exist 60 | if ( -not (Test-Path -Path "${PSScriptRoot}\..\tmp\chocolatey\$windowsSdk\$windowsSdk.10.0.22621.2.nupkg" -PathType Leaf) ) { 61 | Save-ChocoPackage -PackageName $windowsSdk 62 | } 63 | -------------------------------------------------------------------------------- /scripts/prebuild.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import os from 'node:os'; 4 | import fs from 'node:fs'; 5 | import path from 'node:path'; 6 | import process from 'node:process'; 7 | import childProcess from 'node:child_process'; 8 | import url from 'node:url'; 9 | import packageJSON from '../package.json' assert { type: 'json' }; 10 | 11 | const platform = os.platform(); 12 | 13 | const projectPath = path.dirname( 14 | path.dirname(url.fileURLToPath(import.meta.url)), 15 | ); 16 | 17 | /* eslint-disable no-console */ 18 | async function main(argv = process.argv) { 19 | argv = argv.slice(2); 20 | let arch; 21 | // This indicates whether the binary is optimised for production 22 | let production = false; 23 | const restArgs = []; 24 | while (argv.length > 0) { 25 | const option = argv.shift(); 26 | let match; 27 | if ((match = option.match(/--arch(?:=(.+)|$)/))) { 28 | arch = match[1] ?? argv.shift(); 29 | } else if ((match = option.match(/--production$/))) { 30 | production = true; 31 | } else { 32 | restArgs.push(option); 33 | } 34 | } 35 | if (arch == null) { 36 | arch = process.env.npm_config_arch ?? os.arch(); 37 | } 38 | // Derive rustc targets from platform and arch 39 | let targetArch; 40 | switch (arch) { 41 | case 'x64': 42 | targetArch = 'x86_64'; 43 | break; 44 | case 'ia32': 45 | targetArch = 'i686'; 46 | break; 47 | case 'arm64': 48 | targetArch = 'aarch64'; 49 | break; 50 | default: 51 | console.error('Unsupported architecture'); 52 | process.exitCode = 1; 53 | return process.exitCode; 54 | } 55 | let targetVendor; 56 | let targetSystem; 57 | let targetABI; 58 | switch (platform) { 59 | case 'linux': 60 | targetVendor = 'unknown'; 61 | targetSystem = 'linux'; 62 | targetABI = 'gnu'; 63 | break; 64 | case 'darwin': 65 | targetVendor = 'apple'; 66 | targetSystem = 'darwin'; 67 | break; 68 | case 'win32': 69 | targetVendor = 'pc'; 70 | targetSystem = 'windows'; 71 | targetABI = 'msvc'; 72 | break; 73 | default: 74 | console.error('Unsupported platform'); 75 | process.exitCode = 1; 76 | return process.exitCode; 77 | } 78 | const target = [targetArch, targetVendor, targetSystem, targetABI] 79 | .filter((s) => s != null) 80 | .join('-'); 81 | const prebuildPath = path.join(projectPath, 'prebuild'); 82 | await fs.promises.mkdir(prebuildPath, { 83 | recursive: true, 84 | }); 85 | const cargoTOMLPath = path.join(projectPath, 'Cargo.toml'); 86 | const cargoTOML = await fs.promises.readFile(cargoTOMLPath, 'utf-8'); 87 | const cargoTOMLVersion = cargoTOML.match(/version\s*=\s*"(.*)"/)?.[1]; 88 | if (packageJSON.version !== cargoTOMLVersion) { 89 | console.error( 90 | 'Make sure that Cargo.toml version matches the package.json version', 91 | ); 92 | process.exitCode = 1; 93 | return process.exitCode; 94 | } 95 | await fs.promises.writeFile(cargoTOMLPath, cargoTOML, { encoding: 'utf-8' }); 96 | const buildPath = await fs.promises.mkdtemp( 97 | path.join(os.tmpdir(), 'prebuild-'), 98 | ); 99 | const buildArgs = [ 100 | 'build', 101 | buildPath, 102 | `--target=${target}`, 103 | ...(production ? ['--release', '--strip'] : []), 104 | ]; 105 | console.error('Running napi build:'); 106 | console.error(['napi', ...buildArgs].join(' ')); 107 | childProcess.execFileSync('napi', buildArgs, { 108 | stdio: ['inherit', 'inherit', 'inherit'], 109 | windowsHide: true, 110 | encoding: 'utf-8', 111 | shell: platform === 'win32' ? true : false, 112 | }); 113 | // Rename to `name-platform-arch.node` 114 | const buildNames = await fs.promises.readdir(buildPath); 115 | const buildName = buildNames.find((filename) => /\.node$/.test(filename)); 116 | const name = path.basename(buildName, '.node'); 117 | await fs.promises.copyFile( 118 | path.join(buildPath, buildName), 119 | path.join(prebuildPath, `${name}-${platform}-${arch}.node`), 120 | ); 121 | await fs.promises.rm(buildPath, { 122 | recursive: true, 123 | }); 124 | } 125 | /* eslint-enable no-console */ 126 | 127 | if (import.meta.url.startsWith('file:')) { 128 | const modulePath = url.fileURLToPath(import.meta.url); 129 | if (process.argv[1] === modulePath) { 130 | void main(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /scripts/prepublishOnly.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This runs before `npm publish` command. 5 | * This will take the native objects in `prebuild/` 6 | * and create native packages under `prepublishOnly/`. 7 | * 8 | * For example: 9 | * 10 | * /prepublishOnly 11 | * /@org 12 | * /name-linux-x64 13 | * /package.json 14 | * /node.napi.node 15 | * /README.md 16 | */ 17 | 18 | import os from 'node:os'; 19 | import fs from 'node:fs'; 20 | import path from 'node:path'; 21 | import process from 'node:process'; 22 | import childProcess from 'node:child_process'; 23 | import url from 'node:url'; 24 | import packageJSON from '../package.json' assert { type: 'json' }; 25 | 26 | const platform = os.platform(); 27 | 28 | const projectPath = path.dirname( 29 | path.dirname(url.fileURLToPath(import.meta.url)), 30 | ); 31 | 32 | /* eslint-disable no-console */ 33 | async function main(argv = process.argv) { 34 | argv = argv.slice(2); 35 | let tag; 36 | let dryRun = false; 37 | const restArgs = []; 38 | while (argv.length > 0) { 39 | const option = argv.shift(); 40 | let match; 41 | if ((match = option.match(/--tag(?:=(.+)|$)/))) { 42 | tag = match[1] ?? argv.shift(); 43 | } else if ((match = option.match(/--dry-run$/))) { 44 | dryRun = true; 45 | } else { 46 | restArgs.push(option); 47 | } 48 | } 49 | if (tag == null) { 50 | tag = process.env.npm_config_tag; 51 | } 52 | const prebuildPath = path.join(projectPath, 'prebuild'); 53 | const prepublishOnlyPath = path.join(projectPath, 'prepublishOnly'); 54 | const buildNames = (await fs.promises.readdir(prebuildPath)).filter( 55 | (filename) => /^(?:[^-]+)-(?:[^-]+)-(?:[^-]+)$/.test(filename), 56 | ); 57 | if (buildNames.length < 1) { 58 | console.error( 59 | 'You must prebuild at least 1 native object with the filename of `name-platform-arch` before prepublish', 60 | ); 61 | process.exitCode = 1; 62 | return process.exitCode; 63 | } 64 | // Extract out the org name, this may be undefined 65 | const orgName = packageJSON.name.match(/^@[^/]+/)?.[0]; 66 | for (const buildName of buildNames) { 67 | // This is `name-platform-arch` 68 | const name = path.basename(buildName, '.node'); 69 | // This is `@org/name-platform-arch`, uses `posix` to force usage of `/` 70 | let packageName = path.posix.join(orgName ?? '', name); 71 | // Check and rename any universal packages as universal 72 | if (packageName.includes('+')) { 73 | const packageNameSplit = packageName.split('-'); 74 | packageNameSplit[2] = 'universal'; 75 | packageName = packageNameSplit.join('-'); 76 | } 77 | const constraints = name.match( 78 | /^(?:[^-]+)-(?[^-]+)-(?[^-]+)$/, 79 | ); 80 | // This will be `prebuild/name-platform-arch.node` 81 | const buildPath = path.join(prebuildPath, buildName); 82 | // This will be `prepublishOnly/@org/name-platform-arch` 83 | const packagePath = path.join(prepublishOnlyPath, packageName); 84 | console.error('Packaging:', packagePath); 85 | try { 86 | await fs.promises.rm(packagePath, { 87 | recursive: true, 88 | }); 89 | } catch (e) { 90 | if (e.code !== 'ENOENT') throw e; 91 | } 92 | await fs.promises.mkdir(packagePath, { recursive: true }); 93 | const nativePackageJSON = { 94 | name: packageName, 95 | version: packageJSON.version, 96 | homepage: packageJSON.homepage, 97 | author: packageJSON.author, 98 | contributors: packageJSON.contributors, 99 | description: packageJSON.description, 100 | keywords: packageJSON.keywords, 101 | license: packageJSON.license, 102 | repository: packageJSON.repository, 103 | main: 'node.napi.node', 104 | os: [constraints.groups.platform], 105 | cpu: [...constraints.groups.arch.split('+')], 106 | }; 107 | const packageJSONPath = path.join(packagePath, 'package.json'); 108 | console.error(`Writing ${packageJSONPath}`); 109 | const packageJSONString = JSON.stringify(nativePackageJSON, null, 2); 110 | console.error(packageJSONString); 111 | await fs.promises.writeFile(packageJSONPath, packageJSONString, { 112 | encoding: 'utf-8', 113 | }); 114 | const packageReadmePath = path.join(packagePath, 'README.md'); 115 | console.error(`Writing ${packageReadmePath}`); 116 | const packageReadme = `# ${packageName}\n`; 117 | console.error(packageReadme); 118 | await fs.promises.writeFile(packageReadmePath, packageReadme, { 119 | encoding: 'utf-8', 120 | }); 121 | const packageBuildPath = path.join(packagePath, 'node.napi.node'); 122 | console.error(`Copying ${buildPath} to ${packageBuildPath}`); 123 | await fs.promises.copyFile(buildPath, packageBuildPath); 124 | const publishArgs = [ 125 | 'publish', 126 | packagePath, 127 | ...(tag != null ? [`--tag=${tag}`] : []), 128 | '--access=public', 129 | ...(dryRun ? ['--dry-run'] : []), 130 | ]; 131 | console.error('Running npm publish:'); 132 | console.error(['npm', ...publishArgs].join(' ')); 133 | childProcess.execFileSync('npm', publishArgs, { 134 | stdio: ['inherit', 'inherit', 'inherit'], 135 | windowsHide: true, 136 | encoding: 'utf-8', 137 | shell: platform === 'win32' ? true : false, 138 | }); 139 | } 140 | } 141 | /* eslint-enable no-console */ 142 | 143 | void main(); 144 | -------------------------------------------------------------------------------- /scripts/test.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | import url from 'node:url'; 6 | import process from 'node:process'; 7 | import childProcess from 'node:child_process'; 8 | 9 | const projectPath = path.dirname( 10 | path.dirname(url.fileURLToPath(import.meta.url)), 11 | ); 12 | 13 | const platform = os.platform(); 14 | 15 | /* eslint-disable no-console */ 16 | async function main(argv = process.argv) { 17 | argv = argv.slice(2); 18 | const tscArgs = [`-p`, path.join(projectPath, 'tsconfig.build.json')]; 19 | console.error('Running tsc:'); 20 | console.error(['tsc', ...tscArgs].join(' ')); 21 | childProcess.execFileSync('tsc', tscArgs, { 22 | stdio: ['inherit', 'inherit', 'inherit'], 23 | windowsHide: true, 24 | encoding: 'utf-8', 25 | shell: platform === 'win32' ? true : false, 26 | }); 27 | const jestArgs = [...argv]; 28 | console.error('Running jest:'); 29 | console.error(['jest', ...jestArgs].join(' ')); 30 | childProcess.execFileSync('jest', jestArgs, { 31 | env: { 32 | ...process.env, 33 | NODE_OPTIONS: '--experimental-vm-modules', 34 | }, 35 | stdio: ['inherit', 'inherit', 'inherit'], 36 | windowsHide: true, 37 | encoding: 'utf-8', 38 | shell: platform === 'win32' ? true : false, 39 | }); 40 | } 41 | /* eslint-enable no-console */ 42 | 43 | if (import.meta.url.startsWith('file:')) { 44 | const modulePath = url.fileURLToPath(import.meta.url); 45 | if (process.argv[1] === modulePath) { 46 | void main(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/version.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This runs after `npm version` command updates the version but before changes are commited. 5 | * This will update the `cargo.toml` version to match the new `package.json` verson. 6 | * This will also update the `package.json` optional native dependencies 7 | * to match the same version as the version of this package. 8 | * This maintains the same version between this master package 9 | * and the optional native dependencies. 10 | * At the same time, the `package-lock.json` is also regenerated. 11 | * Note that at this point, the new optional native dependencies have 12 | * not yet been published, so the `--package-lock-only` flag is used 13 | * to prevent `npm` from attempting to download unpublished packages. 14 | */ 15 | 16 | import path from 'node:path'; 17 | import fs from 'node:fs'; 18 | import os from 'node:os'; 19 | import url from 'node:url'; 20 | import childProcess from 'node:child_process'; 21 | import packageJSON from '../package.json' assert { type: 'json' }; 22 | 23 | const platform = os.platform(); 24 | 25 | const projectPath = path.dirname( 26 | path.dirname(url.fileURLToPath(import.meta.url)), 27 | ); 28 | 29 | /* eslint-disable no-console */ 30 | async function main() { 31 | const cargoTOMLPath = path.join(projectPath, 'Cargo.toml'); 32 | const cargoLockPath = path.join(projectPath, 'Cargo.lock'); 33 | 34 | console.error('Updating the Cargo.toml version to match new version'); 35 | const cargoTOML = await fs.promises.readFile(cargoTOMLPath, 'utf-8'); 36 | const cargoTOMLMatch = cargoTOML.match(/version\s*=\s*"(.*)"/); 37 | const cargoTOMLUpdated = cargoTOML.replace( 38 | cargoTOMLMatch[0], 39 | `version = "${packageJSON.version}"`, 40 | ); 41 | await fs.promises.writeFile(cargoTOMLPath, cargoTOMLUpdated, 'utf-8'); 42 | 43 | console.error('Updating the Cargo.lock version to match new version'); 44 | childProcess.execFileSync('cargo', ['update', '--package', 'quic'], { 45 | stdio: ['inherit', 'inherit', 'inherit'], 46 | windowsHide: true, 47 | encoding: 'utf-8', 48 | shell: platform === 'win32' ? true : false, 49 | }); 50 | 51 | console.error('Staging Cargo.toml and Cargo.lock'); 52 | childProcess.execFileSync('git', ['add', cargoTOMLPath, cargoLockPath], { 53 | stdio: ['inherit', 'inherit', 'inherit'], 54 | windowsHide: true, 55 | encoding: 'utf-8', 56 | shell: platform === 'win32' ? true : false, 57 | }); 58 | 59 | console.error( 60 | 'Updating the package.json with optional native dependencies and package-lock.json', 61 | ); 62 | const optionalDepsNative = []; 63 | for (const key in packageJSON.optionalDependencies) { 64 | if (key.startsWith(packageJSON.name)) { 65 | optionalDepsNative.push(`${key}@${packageJSON.version}`); 66 | } 67 | } 68 | if (optionalDepsNative.length > 0) { 69 | const installArgs = [ 70 | 'install', 71 | '--ignore-scripts', 72 | '--silent', 73 | '--package-lock-only', 74 | '--save-optional', 75 | '--save-exact', 76 | ...optionalDepsNative, 77 | ]; 78 | console.error('Running npm install:'); 79 | console.error(['npm', ...installArgs].join(' ')); 80 | childProcess.execFileSync('npm', installArgs, { 81 | stdio: ['inherit', 'inherit', 'inherit'], 82 | windowsHide: true, 83 | encoding: 'utf-8', 84 | shell: platform === 'win32' ? true : false, 85 | }); 86 | console.error('Running npm install again to update the package-lock.json:'); 87 | const installArgs_ = [ 88 | 'install', 89 | '--ignore-scripts', 90 | '--silent', 91 | '--package-lock-only', 92 | ]; 93 | childProcess.execFileSync('npm', installArgs_, { 94 | stdio: ['inherit', 'inherit', 'inherit'], 95 | windowsHide: true, 96 | encoding: 'utf-8', 97 | shell: platform === 'win32' ? true : false, 98 | }); 99 | } 100 | } 101 | /* eslint-enable no-console */ 102 | 103 | void main(); 104 | -------------------------------------------------------------------------------- /src/QUICConnectionId.ts: -------------------------------------------------------------------------------- 1 | class QUICConnectionId extends Uint8Array { 2 | public readonly string: string; 3 | 4 | /** 5 | * Decodes from hex string. 6 | */ 7 | public static fromString(idString: string): QUICConnectionId { 8 | const buf = Buffer.from(idString, 'hex'); 9 | return new this(buf.buffer, buf.byteOffset, buf.byteLength); 10 | } 11 | 12 | /** 13 | * Decodes as Buffer zero-copy. 14 | */ 15 | public static fromBuffer(idBuffer: Buffer): QUICConnectionId { 16 | return new this(idBuffer.buffer, idBuffer.byteOffset, idBuffer.byteLength); 17 | } 18 | 19 | public constructor(); 20 | public constructor(length: number); 21 | public constructor(array: ArrayLike | ArrayBufferLike); 22 | public constructor( 23 | buffer: ArrayBufferLike, 24 | byteOffset?: number, 25 | length?: number, 26 | ); 27 | public constructor(...args: Array) { 28 | // @ts-ignore: spreading into Uint8Array constructor 29 | super(...args); 30 | this.string = this.toBuffer().toString('hex'); 31 | } 32 | 33 | /** 34 | * Encodes to hex string. 35 | */ 36 | public toString(): string { 37 | return this.string; 38 | } 39 | 40 | /** 41 | * Encodes as Buffer zero-copy. 42 | */ 43 | public toBuffer(): Buffer { 44 | return Buffer.from(this.buffer, this.byteOffset, this.byteLength); 45 | } 46 | 47 | public [Symbol.toPrimitive](_hint: 'string' | 'number' | 'default'): string { 48 | return this.toString(); 49 | } 50 | } 51 | 52 | export default QUICConnectionId; 53 | -------------------------------------------------------------------------------- /src/QUICConnectionMap.ts: -------------------------------------------------------------------------------- 1 | import type QUICConnection from './QUICConnection.js'; 2 | import QUICConnectionId from './QUICConnectionId.js'; 3 | 4 | class QUICConnectionMap implements Map { 5 | public [Symbol.toStringTag]: string = 'QUICConnectionMap'; 6 | protected _serverConnections: Map = new Map(); 7 | protected _clientConnections: Map = new Map(); 8 | 9 | public constructor( 10 | connections?: Iterable, 11 | ) { 12 | if (connections != null) { 13 | for (const [connectionId, connection] of connections) { 14 | this.set(connectionId, connection); 15 | } 16 | } 17 | } 18 | 19 | public get size(): number { 20 | return this._serverConnections.size + this._clientConnections.size; 21 | } 22 | 23 | /** 24 | * Gets the server connections. 25 | * This uses `ConnectionIdString` because it is too complex to map 26 | * `ConnectionId` to `ConnectionIdString` and back. 27 | */ 28 | public get serverConnections(): ReadonlyMap { 29 | return this._serverConnections; 30 | } 31 | 32 | /** 33 | * Gets the client connections. 34 | * This uses `ConnectionIdString` because it is too complex to map 35 | * `ConnectionId` to `ConnectionIdString` and back. 36 | */ 37 | public get clientConnections(): ReadonlyMap { 38 | return this._clientConnections; 39 | } 40 | 41 | public has(connectionId: QUICConnectionId): boolean { 42 | return ( 43 | this._serverConnections.has(connectionId.toString()) || 44 | this._clientConnections.has(connectionId.toString()) 45 | ); 46 | } 47 | 48 | public get(connectionId: QUICConnectionId): QUICConnection | undefined { 49 | return ( 50 | this._serverConnections.get(connectionId.toString()) ?? 51 | this._clientConnections.get(connectionId.toString()) 52 | ); 53 | } 54 | 55 | public set(connectionId: QUICConnectionId, connection: QUICConnection): this { 56 | if (connection.type === 'server') { 57 | this._serverConnections.set(connectionId.toString(), connection); 58 | } else if (connection.type === 'client') { 59 | this._clientConnections.set(connectionId.toString(), connection); 60 | } 61 | return this; 62 | } 63 | 64 | public delete(connectionId: QUICConnectionId): boolean { 65 | return ( 66 | this._serverConnections.delete(connectionId.toString()) || 67 | this._clientConnections.delete(connectionId.toString()) 68 | ); 69 | } 70 | 71 | public clear(): void { 72 | this._serverConnections.clear(); 73 | this._clientConnections.clear(); 74 | } 75 | 76 | public forEach( 77 | callback: ( 78 | value: QUICConnection, 79 | key: QUICConnectionId, 80 | map: Map, 81 | ) => void, 82 | thisArg?: any, 83 | ): void { 84 | this._serverConnections.forEach((value, key) => { 85 | callback.bind(thisArg)(value, QUICConnectionId.fromString(key), this); 86 | }); 87 | this._clientConnections.forEach((value, key) => { 88 | callback.bind(thisArg)(value, QUICConnectionId.fromString(key), this); 89 | }); 90 | } 91 | 92 | public [Symbol.iterator](): IterableIterator< 93 | [QUICConnectionId, QUICConnection] 94 | > { 95 | const serverIterator = this._serverConnections[Symbol.iterator](); 96 | const clientIterator = this._clientConnections[Symbol.iterator](); 97 | const iterator = { 98 | next: (): IteratorResult<[QUICConnectionId, QUICConnection], void> => { 99 | const serverResult = serverIterator.next(); 100 | if (!serverResult.done) { 101 | const [key, value] = serverResult.value; 102 | return { 103 | done: false, 104 | value: [QUICConnectionId.fromString(key), value], 105 | }; 106 | } 107 | const clientResult = clientIterator.next(); 108 | if (!clientResult.done) { 109 | const [key, value] = clientResult.value; 110 | return { 111 | done: false, 112 | value: [QUICConnectionId.fromString(key), value], 113 | }; 114 | } 115 | return { done: true, value: undefined }; 116 | }, 117 | [Symbol.iterator]: () => iterator, 118 | }; 119 | return iterator; 120 | } 121 | 122 | public entries(): IterableIterator<[QUICConnectionId, QUICConnection]> { 123 | return this[Symbol.iterator](); 124 | } 125 | 126 | public keys(): IterableIterator { 127 | const iterator = { 128 | next: (): IteratorResult => { 129 | const result = this[Symbol.iterator]().next(); 130 | if (!result.done) { 131 | return { 132 | done: false, 133 | value: result.value[0], 134 | }; 135 | } 136 | return { done: true, value: undefined }; 137 | }, 138 | [Symbol.iterator]: () => iterator, 139 | }; 140 | return iterator; 141 | } 142 | 143 | public values(): IterableIterator { 144 | const iterator = { 145 | next: (): IteratorResult => { 146 | const result = this[Symbol.iterator]().next(); 147 | if (!result.done) { 148 | return { 149 | done: false, 150 | value: result.value[1], 151 | }; 152 | } 153 | return { done: true, value: undefined }; 154 | }, 155 | [Symbol.iterator]: () => iterator, 156 | }; 157 | return iterator; 158 | } 159 | } 160 | 161 | export default QUICConnectionMap; 162 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { QUICConfig } from './types.js'; 2 | import type { Config as QuicheConfig } from './native/types.js'; 3 | import quiche from './native/quiche.js'; 4 | import * as utils from './utils.js'; 5 | import * as errors from './errors.js'; 6 | 7 | /** 8 | * BoringSSL does not support: 9 | * - rsa_pss_pss_sha256 10 | * - rsa_pss_pss_sha384 11 | * - rsa_pss_pss_sha512 12 | * - ed448 13 | */ 14 | const sigalgs = [ 15 | 'rsa_pkcs1_sha256', 16 | 'rsa_pkcs1_sha384', 17 | 'rsa_pkcs1_sha512', 18 | 'rsa_pss_rsae_sha256', 19 | 'rsa_pss_rsae_sha384', 20 | 'rsa_pss_rsae_sha512', 21 | 'ecdsa_secp256r1_sha256', 22 | 'ecdsa_secp384r1_sha384', 23 | 'ecdsa_secp521r1_sha512', 24 | 'ed25519', 25 | ].join(':'); 26 | 27 | /** 28 | * Usually we would create separate timeouts for starting vs keep-alive. 29 | * Unfortunately quiche only has 1 config option that controls both. 30 | * And it is not possible to mutate this option after connecting. 31 | * Therefore, this option is just a way to set a shorter start timeout 32 | * compared to the idling timeout. 33 | * If this is the larger than the `maxIdleTimeout` (where `0` means `Infinity`), 34 | * then this has no effect. This only has an effect if this is set to a number 35 | * less than `maxIdleTimeout`. Thus, it is the "minimum boundary" of the 36 | * timeout when starting. While the `maxIdleTimeout` is still the "maximum 37 | * boundary" when starting. 38 | * Both `minIdleTimeout` and `maxIdleTimeout` defaults to `Infinity` (where `0` 39 | * means `Infinity` for `maxIdleTimeout`), thus by default connections will not 40 | * time out when starting or during keep-alive. 41 | */ 42 | const minIdleTimeout = Infinity; 43 | 44 | const clientDefault: QUICConfig = { 45 | sigalgs, 46 | verifyPeer: true, 47 | grease: true, 48 | keepAliveIntervalTime: undefined, 49 | maxIdleTimeout: 0, 50 | maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 65527, but set to 1350 51 | maxSendUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 1200, but set to 1350 52 | initialMaxData: 10 * 1024 * 1024, 53 | initialMaxStreamDataBidiLocal: 1 * 1024 * 1024, 54 | initialMaxStreamDataBidiRemote: 1 * 1024 * 1024, 55 | initialMaxStreamDataUni: 1 * 1024 * 1024, 56 | initialMaxStreamsBidi: 100, 57 | initialMaxStreamsUni: 100, 58 | maxConnectionWindow: quiche.MAX_CONNECTION_WINDOW, 59 | maxStreamWindow: quiche.MAX_STREAM_WINDOW, 60 | enableDgram: [false, 0, 0], 61 | disableActiveMigration: true, 62 | applicationProtos: ['quic'], 63 | enableEarlyData: true, 64 | readableChunkSize: 4 * 1024, 65 | }; 66 | 67 | const serverDefault: QUICConfig = { 68 | sigalgs, 69 | verifyPeer: false, 70 | grease: true, 71 | keepAliveIntervalTime: undefined, 72 | maxIdleTimeout: 0, 73 | maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 65527 74 | maxSendUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // Default is 1200, but set to 1350 75 | initialMaxData: 10 * 1024 * 1024, 76 | initialMaxStreamDataBidiLocal: 1 * 1024 * 1024, 77 | initialMaxStreamDataBidiRemote: 1 * 1024 * 1024, 78 | initialMaxStreamDataUni: 1 * 1024 * 1024, 79 | initialMaxStreamsBidi: 100, 80 | initialMaxStreamsUni: 100, 81 | maxConnectionWindow: quiche.MAX_CONNECTION_WINDOW, 82 | maxStreamWindow: quiche.MAX_STREAM_WINDOW, 83 | enableDgram: [false, 0, 0], 84 | disableActiveMigration: true, 85 | applicationProtos: ['quic'], 86 | enableEarlyData: true, 87 | readableChunkSize: 4 * 1024, 88 | }; 89 | 90 | /** 91 | * Converts QUICConfig to QuicheConfig. 92 | * This does not use all the options of QUICConfig. 93 | * The QUICConfig is still necessary. 94 | */ 95 | function buildQuicheConfig(config: QUICConfig): QuicheConfig { 96 | if (config.key != null && config.cert == null) { 97 | throw new errors.ErrorQUICConfig( 98 | 'The cert option must be set when key is set', 99 | ); 100 | } else if (config.key == null && config.cert != null) { 101 | throw new errors.ErrorQUICConfig( 102 | 'The key option must be set when cert is set', 103 | ); 104 | } else if (config.key != null && config.cert != null) { 105 | if (Array.isArray(config.key) && Array.isArray(config.cert)) { 106 | if (config.key.length !== config.cert.length) { 107 | throw new errors.ErrorQUICConfig( 108 | 'The number of keys must match the number of certs', 109 | ); 110 | } 111 | } 112 | } 113 | // This is a concatenated CA certificates in PEM format 114 | let caPEMBuffer: Uint8Array | undefined; 115 | if (config.ca != null) { 116 | const caPEMBuffers = utils.collectPEMs(config.ca); 117 | caPEMBuffer = utils.textEncoder.encode(caPEMBuffers.join('')); 118 | } 119 | // This is an array of private keys in PEM format as buffers 120 | let keyPEMBuffers: Array | undefined; 121 | if (config.key != null) { 122 | const keyPEMs = utils.collectPEMs(config.key); 123 | keyPEMBuffers = keyPEMs.map((k) => utils.textEncoder.encode(k)); 124 | } 125 | // This is an array of certificate chains in PEM format as buffers 126 | let certChainPEMBuffers: Array | undefined; 127 | if (config.cert != null) { 128 | const certPEMsChain = utils.collectPEMs(config.cert); 129 | certChainPEMBuffers = certPEMsChain.map((c) => utils.textEncoder.encode(c)); 130 | } 131 | let quicheConfig: QuicheConfig; 132 | try { 133 | quicheConfig = quiche.Config.withBoringSslCtx( 134 | config.verifyPeer, 135 | config.verifyCallback != null, 136 | caPEMBuffer, 137 | keyPEMBuffers, 138 | certChainPEMBuffers, 139 | config.sigalgs, 140 | ); 141 | } catch (e) { 142 | throw new errors.ErrorQUICConfig( 143 | `Failed to build Quiche config with custom SSL context: ${e.message}`, 144 | { cause: e }, 145 | ); 146 | } 147 | if (config.logKeys != null) { 148 | quicheConfig.logKeys(); 149 | } 150 | if (config.enableEarlyData) { 151 | quicheConfig.enableEarlyData(); 152 | } 153 | quicheConfig.grease(config.grease); 154 | quicheConfig.setMaxIdleTimeout(config.maxIdleTimeout); 155 | quicheConfig.setMaxRecvUdpPayloadSize(config.maxRecvUdpPayloadSize); 156 | quicheConfig.setMaxSendUdpPayloadSize(config.maxSendUdpPayloadSize); 157 | quicheConfig.setInitialMaxData(config.initialMaxData); 158 | quicheConfig.setInitialMaxStreamDataBidiLocal( 159 | config.initialMaxStreamDataBidiLocal, 160 | ); 161 | quicheConfig.setInitialMaxStreamDataBidiRemote( 162 | config.initialMaxStreamDataBidiRemote, 163 | ); 164 | quicheConfig.setInitialMaxStreamDataUni(config.initialMaxStreamDataUni); 165 | quicheConfig.setInitialMaxStreamsBidi(config.initialMaxStreamsBidi); 166 | quicheConfig.setInitialMaxStreamsUni(config.initialMaxStreamsUni); 167 | quicheConfig.enableDgram(...config.enableDgram); 168 | quicheConfig.setDisableActiveMigration(config.disableActiveMigration); 169 | quicheConfig.setApplicationProtos(config.applicationProtos); 170 | return quicheConfig; 171 | } 172 | 173 | export { minIdleTimeout, clientDefault, serverDefault, buildQuicheConfig }; 174 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import type { POJO } from './types.js'; 2 | import type { ConnectionError, CryptoError } from './native/types.js'; 3 | import { AbstractError } from '@matrixai/errors'; 4 | 5 | class ErrorQUIC extends AbstractError { 6 | static description = 'QUIC error'; 7 | } 8 | 9 | class ErrorQUICUndefinedBehaviour extends ErrorQUIC { 10 | static description = 'You should never see this error'; 11 | } 12 | 13 | class ErrorQUICHostInvalid extends ErrorQUIC { 14 | static description = 'Host provided was not valid'; 15 | } 16 | 17 | class ErrorQUICPortInvalid extends ErrorQUIC { 18 | static description = 'Port provided was not valid'; 19 | } 20 | 21 | class ErrorQUICConfig extends ErrorQUIC { 22 | static description = 'QUIC config error'; 23 | } 24 | 25 | class ErrorQUICSocket extends ErrorQUIC { 26 | static description = 'QUIC Socket error'; 27 | } 28 | 29 | class ErrorQUICSocketNotRunning extends ErrorQUICSocket { 30 | static description = 'QUIC Socket is not running'; 31 | } 32 | 33 | class ErrorQUICSocketConnectionsActive extends ErrorQUICSocket { 34 | static description = 'QUIC Socket has active connections'; 35 | } 36 | 37 | class ErrorQUICSocketInvalidBindAddress extends ErrorQUICSocket { 38 | static description = 'QUIC Socket cannot bind to the specified address'; 39 | } 40 | 41 | class ErrorQUICSocketInvalidSendAddress extends ErrorQUICSocket { 42 | static description = 'QUIC Socket cannot send to the specified address'; 43 | } 44 | 45 | class ErrorQUICSocketInternal extends ErrorQUICSocket { 46 | static description = 'QUIC Socket internal error'; 47 | } 48 | 49 | class ErrorQUICClient extends ErrorQUIC { 50 | static description = 'QUIC Client error'; 51 | } 52 | 53 | class ErrorQUICClientDestroyed extends ErrorQUICClient { 54 | static description = 'QUIC Client is destroyed'; 55 | } 56 | 57 | class ErrorQUICClientCreateTimeout extends ErrorQUICClient { 58 | static description = 'QUIC Client create timeout'; 59 | } 60 | 61 | class ErrorQUICClientSocketNotRunning extends ErrorQUICClient { 62 | static description = 63 | 'QUIC Client cannot be created with an unstarted shared QUIC socket'; 64 | } 65 | 66 | class ErrorQUICClientInvalidArgument extends ErrorQUICClient { 67 | static description = 68 | 'QUIC Client had a failure relating to an invalid argument'; 69 | } 70 | 71 | class ErrorQUICClientInvalidHost extends ErrorQUICClient { 72 | static description = 'QUIC Client cannot be created with the specified host'; 73 | } 74 | 75 | class ErrorQUICClientInternal extends ErrorQUICClient { 76 | static description = 'QUIC Client internal error'; 77 | } 78 | 79 | class ErrorQUICServer extends ErrorQUIC { 80 | static description = 'QUIC Server error'; 81 | } 82 | 83 | class ErrorQUICServerNotRunning extends ErrorQUICServer { 84 | static description = 'QUIC Server is not running'; 85 | } 86 | 87 | class ErrorQUICServerSocketNotRunning extends ErrorQUICServer { 88 | static description = 89 | 'QUIC Server cannot start with an unstarted shared QUIC socket'; 90 | } 91 | 92 | class ErrorQUICServerNewConnection extends ErrorQUICServer { 93 | static description = 'QUIC Server creating a new connection'; 94 | } 95 | 96 | class ErrorQUICServerInternal extends ErrorQUICServer { 97 | static description = 'QUIC Server internal error'; 98 | } 99 | 100 | class ErrorQUICServerStopping extends ErrorQUICServer { 101 | static description = 'QUIC Server is stopping'; 102 | } 103 | 104 | class ErrorQUICConnection extends ErrorQUIC { 105 | static description = 'QUIC Connection error'; 106 | } 107 | 108 | class ErrorQUICConnectionStopping extends ErrorQUICConnection { 109 | static description = 'QUIC Connection is stopping'; 110 | } 111 | 112 | class ErrorQUICConnectionNotRunning extends ErrorQUICConnection { 113 | static description = 'QUIC Connection is not running'; 114 | } 115 | 116 | class ErrorQUICConnectionClosed extends ErrorQUICConnection { 117 | static description = 118 | 'QUIC Connection cannot be restarted because it has already been closed'; 119 | } 120 | 121 | class ErrorQUICConnectionStartData extends ErrorQUIC { 122 | static description = 123 | 'QUIC Connection start requires data when it is a server connection'; 124 | } 125 | 126 | class ErrorQUICConnectionStartTimeout extends ErrorQUICConnection { 127 | static description = 'QUIC Connection start timeout'; 128 | } 129 | 130 | class ErrorQUICConnectionConfigInvalid extends ErrorQUICConnection { 131 | static description = 'QUIC connection invalid configuration'; 132 | } 133 | 134 | class ErrorQUICConnectionLocal extends ErrorQUICConnection { 135 | static description = 'QUIC Connection local error'; 136 | declare data: POJO & ConnectionError; 137 | constructor( 138 | message: string = '', 139 | options: { 140 | timestamp?: Date; 141 | data: POJO & ConnectionError; 142 | cause?: T; 143 | }, 144 | ) { 145 | super(message, options); 146 | } 147 | } 148 | 149 | class ErrorQUICConnectionLocalTLS extends ErrorQUICConnectionLocal { 150 | static description = 'QUIC Connection local TLS error'; 151 | declare data: POJO & 152 | ConnectionError & { 153 | errorCode: CryptoError; 154 | }; 155 | constructor( 156 | message: string = '', 157 | options: { 158 | timestamp?: Date; 159 | data: POJO & 160 | ConnectionError & { 161 | errorCode: CryptoError; 162 | }; 163 | cause?: T; 164 | }, 165 | ) { 166 | super(message, options); 167 | } 168 | } 169 | 170 | class ErrorQUICConnectionPeer extends ErrorQUICConnection { 171 | static description = 'QUIC Connection peer error'; 172 | declare data: POJO & ConnectionError; 173 | constructor( 174 | message: string = '', 175 | options: { 176 | timestamp?: Date; 177 | data: POJO & ConnectionError; 178 | cause?: T; 179 | }, 180 | ) { 181 | super(message, options); 182 | } 183 | } 184 | 185 | class ErrorQUICConnectionPeerTLS extends ErrorQUICConnectionLocal { 186 | static description = 'QUIC Connection local TLS error'; 187 | declare data: POJO & 188 | ConnectionError & { 189 | errorCode: CryptoError; 190 | }; 191 | constructor( 192 | message: string = '', 193 | options: { 194 | timestamp?: Date; 195 | data: POJO & 196 | ConnectionError & { 197 | errorCode: CryptoError; 198 | }; 199 | cause?: T; 200 | }, 201 | ) { 202 | super(message, options); 203 | } 204 | } 205 | 206 | class ErrorQUICConnectionIdleTimeout extends ErrorQUICConnection { 207 | static description = 'QUIC Connection max idle timeout exhausted'; 208 | } 209 | 210 | class ErrorQUICConnectionInternal extends ErrorQUICConnection { 211 | static description = 'QUIC Connection internal error'; 212 | } 213 | 214 | class ErrorQUICStream extends ErrorQUIC { 215 | static description = 'QUIC Stream error'; 216 | } 217 | 218 | class ErrorQUICStreamDestroyed extends ErrorQUICStream { 219 | static description = 'QUIC Stream is destroyed'; 220 | } 221 | 222 | class ErrorQUICStreamLocalRead extends ErrorQUICStream { 223 | static description = 'QUIC Stream locally closed readable side'; 224 | declare data: POJO & { code: number }; 225 | constructor( 226 | message: string = '', 227 | options: { 228 | timestamp?: Date; 229 | data: POJO & { 230 | code: number; 231 | }; 232 | cause?: T; 233 | }, 234 | ) { 235 | super(message, options); 236 | } 237 | } 238 | 239 | class ErrorQUICStreamLocalWrite extends ErrorQUICStream { 240 | static description = 'QUIC Stream locally closed writable side'; 241 | declare data: POJO & { code: number }; 242 | constructor( 243 | message: string = '', 244 | options: { 245 | timestamp?: Date; 246 | data: POJO & { 247 | code: number; 248 | }; 249 | cause?: T; 250 | }, 251 | ) { 252 | super(message, options); 253 | } 254 | } 255 | 256 | class ErrorQUICStreamPeerRead extends ErrorQUICStream { 257 | static description = 'QUIC Stream peer closed readable side'; 258 | declare data: POJO & { code: number }; 259 | constructor( 260 | message: string = '', 261 | options: { 262 | timestamp?: Date; 263 | data: POJO & { 264 | code: number; 265 | }; 266 | cause?: T; 267 | }, 268 | ) { 269 | super(message, options); 270 | } 271 | } 272 | 273 | class ErrorQUICStreamPeerWrite extends ErrorQUICStream { 274 | static description = 'QUIC Stream peer closed writable side'; 275 | declare data: POJO & { code: number }; 276 | constructor( 277 | message: string = '', 278 | options: { 279 | timestamp?: Date; 280 | data: POJO & { 281 | code: number; 282 | }; 283 | cause?: T; 284 | }, 285 | ) { 286 | super(message, options); 287 | } 288 | } 289 | 290 | class ErrorQUICStreamInternal extends ErrorQUICStream { 291 | static description = 'QUIC Stream internal error'; 292 | } 293 | 294 | class ErrorQUICStreamLimit extends ErrorQUICStream { 295 | static description = 'QUIC Stream limit has been reached'; 296 | } 297 | 298 | export { 299 | ErrorQUIC, 300 | ErrorQUICUndefinedBehaviour, 301 | ErrorQUICHostInvalid, 302 | ErrorQUICPortInvalid, 303 | ErrorQUICConfig, 304 | ErrorQUICSocket, 305 | ErrorQUICSocketNotRunning, 306 | ErrorQUICSocketConnectionsActive, 307 | ErrorQUICSocketInvalidBindAddress, 308 | ErrorQUICSocketInvalidSendAddress, 309 | ErrorQUICSocketInternal, 310 | ErrorQUICClient, 311 | ErrorQUICClientDestroyed, 312 | ErrorQUICClientCreateTimeout, 313 | ErrorQUICClientSocketNotRunning, 314 | ErrorQUICClientInvalidArgument, 315 | ErrorQUICClientInvalidHost, 316 | ErrorQUICClientInternal, 317 | ErrorQUICServer, 318 | ErrorQUICServerNotRunning, 319 | ErrorQUICServerSocketNotRunning, 320 | ErrorQUICServerNewConnection, 321 | ErrorQUICServerInternal, 322 | ErrorQUICServerStopping, 323 | ErrorQUICConnection, 324 | ErrorQUICConnectionStopping, 325 | ErrorQUICConnectionNotRunning, 326 | ErrorQUICConnectionClosed, 327 | ErrorQUICConnectionStartData, 328 | ErrorQUICConnectionStartTimeout, 329 | ErrorQUICConnectionConfigInvalid, 330 | ErrorQUICConnectionLocal, 331 | ErrorQUICConnectionLocalTLS, 332 | ErrorQUICConnectionPeer, 333 | ErrorQUICConnectionPeerTLS, 334 | ErrorQUICConnectionIdleTimeout, 335 | ErrorQUICConnectionInternal, 336 | ErrorQUICStream, 337 | ErrorQUICStreamDestroyed, 338 | ErrorQUICStreamLocalRead, 339 | ErrorQUICStreamLocalWrite, 340 | ErrorQUICStreamPeerRead, 341 | ErrorQUICStreamPeerWrite, 342 | ErrorQUICStreamInternal, 343 | ErrorQUICStreamLimit, 344 | }; 345 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import type QUICConnection from './QUICConnection.js'; 2 | import type QUICStream from './QUICStream.js'; 3 | import type { 4 | ErrorQUICConnectionLocal, 5 | ErrorQUICConnectionPeer, 6 | ErrorQUICConnectionInternal, 7 | ErrorQUICStreamLocalRead, 8 | ErrorQUICStreamLocalWrite, 9 | ErrorQUICStreamPeerRead, 10 | ErrorQUICStreamPeerWrite, 11 | ErrorQUICStreamInternal, 12 | ErrorQUICConnectionIdleTimeout, 13 | ErrorQUICSocketInternal, 14 | ErrorQUICServerInternal, 15 | ErrorQUICServerSocketNotRunning, 16 | ErrorQUICClientSocketNotRunning, 17 | ErrorQUICClientInternal, 18 | } from './errors.js'; 19 | import { AbstractEvent } from '@matrixai/events'; 20 | 21 | abstract class EventQUIC extends AbstractEvent {} 22 | 23 | // Socket events 24 | 25 | abstract class EventQUICSocket extends EventQUIC {} 26 | 27 | class EventQUICSocketStart extends EventQUICSocket {} 28 | 29 | class EventQUICSocketStarted extends EventQUICSocket {} 30 | 31 | class EventQUICSocketStop extends EventQUICSocket {} 32 | 33 | class EventQUICSocketStopped extends EventQUICSocket {} 34 | 35 | class EventQUICSocketError extends EventQUICSocket< 36 | ErrorQUICSocketInternal 37 | > {} 38 | 39 | class EventQUICSocketClose extends EventQUICSocket< 40 | ErrorQUICSocketInternal | undefined 41 | > {} 42 | 43 | // Client events 44 | 45 | abstract class EventQUICClient extends EventQUIC {} 46 | 47 | class EventQUICClientDestroy extends EventQUICClient {} 48 | 49 | class EventQUICClientDestroyed extends EventQUICClient {} 50 | 51 | /** 52 | * All `EventQUICConnectionError` errors is also `EventQUICClient` errors. 53 | * This is because `QUICClient` is 1 to 1 to `QUICConnection`. 54 | * It's thin wrapper around it. 55 | */ 56 | class EventQUICClientError extends EventQUICClient< 57 | | ErrorQUICClientSocketNotRunning 58 | | ErrorQUICClientInternal 59 | | ErrorQUICConnectionLocal 60 | | ErrorQUICConnectionPeer 61 | | ErrorQUICConnectionIdleTimeout 62 | | ErrorQUICConnectionInternal 63 | > {} 64 | 65 | class EventQUICClientErrorSend extends EventQUICSocket {} 66 | 67 | class EventQUICClientClose extends EventQUICClient< 68 | | ErrorQUICClientSocketNotRunning 69 | | ErrorQUICConnectionLocal 70 | | ErrorQUICConnectionPeer 71 | | ErrorQUICConnectionIdleTimeout 72 | > {} 73 | 74 | // Server events 75 | 76 | abstract class EventQUICServer extends EventQUIC {} 77 | 78 | class EventQUICServerConnection extends EventQUICServer {} 79 | 80 | class EventQUICServerStart extends EventQUICServer {} 81 | 82 | class EventQUICServerStarted extends EventQUICServer {} 83 | 84 | class EventQUICServerStop extends EventQUICServer {} 85 | 86 | class EventQUICServerStopped extends EventQUICServer {} 87 | 88 | class EventQUICServerError extends EventQUICServer< 89 | ErrorQUICServerSocketNotRunning | ErrorQUICServerInternal 90 | > {} 91 | 92 | class EventQUICServerClose extends EventQUICServer< 93 | ErrorQUICServerSocketNotRunning | undefined 94 | > {} 95 | 96 | // Connection events 97 | 98 | abstract class EventQUICConnection extends EventQUIC {} 99 | 100 | class EventQUICConnectionStart extends EventQUICConnection {} 101 | 102 | class EventQUICConnectionStarted extends EventQUICConnection {} 103 | 104 | class EventQUICConnectionStop extends EventQUICConnection {} 105 | 106 | class EventQUICConnectionStopped extends EventQUICConnection {} 107 | 108 | /** 109 | * Closing a quic connection is always an error no matter if it is graceful or 110 | * not. This is due to the utilisation of the error code and reason during 111 | * connection close. Additionally, it is also possible that the QUIC 112 | * connection times out. In this case, quiche does will not send a 113 | * `CONNECTION_CLOSE` frame. 114 | */ 115 | class EventQUICConnectionError extends EventQUICConnection< 116 | | ErrorQUICConnectionLocal 117 | | ErrorQUICConnectionPeer 118 | | ErrorQUICConnectionIdleTimeout 119 | | ErrorQUICConnectionInternal 120 | > {} 121 | 122 | class EventQUICConnectionClose extends EventQUICConnection< 123 | | ErrorQUICConnectionLocal 124 | | ErrorQUICConnectionPeer 125 | | ErrorQUICConnectionIdleTimeout 126 | > {} 127 | 128 | class EventQUICConnectionStream extends EventQUICConnection {} 129 | 130 | class EventQUICConnectionSend extends EventQUICConnection<{ 131 | id: string; 132 | msg: Uint8Array; 133 | port: number; 134 | address: string; 135 | }> {} 136 | 137 | // Stream events 138 | 139 | abstract class EventQUICStream extends EventQUIC {} 140 | 141 | class EventQUICStreamDestroy extends EventQUICStream {} 142 | 143 | class EventQUICStreamDestroyed extends EventQUICStream {} 144 | 145 | /** 146 | * Gracefully closing a QUIC stream does not require an error event. 147 | */ 148 | class EventQUICStreamError extends EventQUICStream< 149 | | ErrorQUICStreamLocalRead 150 | | ErrorQUICStreamLocalWrite 151 | | ErrorQUICStreamPeerRead 152 | | ErrorQUICStreamPeerWrite 153 | | ErrorQUICStreamInternal 154 | > {} 155 | 156 | /** 157 | * QUIC stream readable side is closed. 158 | * 159 | * `ErrorQUICStreamLocalRead` - readable side cancelled locally with code. 160 | * `ErrorQUICStreamPeerRead` - readable side cancelled by peer aborting the 161 | * remote writable side. 162 | * `undefined` - readable side closed gracefully. 163 | */ 164 | class EventQUICStreamCloseRead extends EventQUICStream< 165 | | ErrorQUICStreamLocalRead 166 | | ErrorQUICStreamPeerRead 167 | | undefined 168 | > {} 169 | 170 | /** 171 | * QUIC stream writable side is closed. 172 | * 173 | * `ErrorQUICStreamLocalWrite` - writable side aborted locally with code. 174 | * `ErrorQUICStreamPeerWrite` - writable side aborted by peer cancelling the 175 | * remote readable side. 176 | * `undefined` - writable side closed gracefully. 177 | */ 178 | class EventQUICStreamCloseWrite extends EventQUICStream< 179 | | ErrorQUICStreamLocalWrite 180 | | ErrorQUICStreamPeerWrite 181 | | undefined 182 | > {} 183 | 184 | class EventQUICStreamSend extends EventQUICStream {} 185 | 186 | export { 187 | EventQUIC, 188 | EventQUICSocket, 189 | EventQUICSocketStart, 190 | EventQUICSocketStarted, 191 | EventQUICSocketStop, 192 | EventQUICSocketStopped, 193 | EventQUICSocketError, 194 | EventQUICSocketClose, 195 | EventQUICClient, 196 | EventQUICClientDestroy, 197 | EventQUICClientDestroyed, 198 | EventQUICClientError, 199 | EventQUICClientErrorSend, 200 | EventQUICClientClose, 201 | EventQUICServer, 202 | EventQUICServerStart, 203 | EventQUICServerStarted, 204 | EventQUICServerStop, 205 | EventQUICServerStopped, 206 | EventQUICServerError, 207 | EventQUICServerClose, 208 | EventQUICServerConnection, 209 | EventQUICConnection, 210 | EventQUICConnectionStart, 211 | EventQUICConnectionStarted, 212 | EventQUICConnectionStop, 213 | EventQUICConnectionStopped, 214 | EventQUICConnectionError, 215 | EventQUICConnectionClose, 216 | EventQUICConnectionStream, 217 | EventQUICConnectionSend, 218 | EventQUICStream, 219 | EventQUICStreamDestroy, 220 | EventQUICStreamDestroyed, 221 | EventQUICStreamError, 222 | EventQUICStreamCloseRead, 223 | EventQUICStreamCloseWrite, 224 | EventQUICStreamSend, 225 | }; 226 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as QUICSocket } from './QUICSocket.js'; 2 | export { default as QUICServer } from './QUICServer.js'; 3 | export { default as QUICClient } from './QUICClient.js'; 4 | export { default as QUICConnectionMap } from './QUICConnectionMap.js'; 5 | export { default as QUICConnection } from './QUICConnection.js'; 6 | export { default as QUICConnectionId } from './QUICConnectionId.js'; 7 | export { default as QUICStream } from './QUICStream.js'; 8 | export * as utils from './utils.js'; 9 | export * as events from './events.js'; 10 | export * as errors from './errors.js'; 11 | export * as native from './native/index.js'; 12 | export type * from './types.js'; 13 | -------------------------------------------------------------------------------- /src/native/index.ts: -------------------------------------------------------------------------------- 1 | export { default as quiche } from './quiche.js'; 2 | export type { Quiche } from './quiche.js'; 3 | export * from './types.js'; 4 | -------------------------------------------------------------------------------- /src/native/napi/config.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | use napi::bindgen_prelude::*; 3 | 4 | #[napi] 5 | pub struct Config(pub (crate) quiche::Config); 6 | 7 | /// Equivalent to quiche::CongestionControlAlgorithm 8 | #[napi] 9 | pub enum CongestionControlAlgorithm { 10 | Reno = 0, 11 | CUBIC = 1, 12 | BBR = 2, 13 | } 14 | 15 | impl From for quiche::CongestionControlAlgorithm { 16 | fn from(algo: CongestionControlAlgorithm) -> Self { 17 | match algo { 18 | CongestionControlAlgorithm::Reno => quiche::CongestionControlAlgorithm::Reno, 19 | CongestionControlAlgorithm::CUBIC => quiche::CongestionControlAlgorithm::CUBIC, 20 | CongestionControlAlgorithm::BBR => quiche::CongestionControlAlgorithm::BBR, 21 | } 22 | } 23 | } 24 | 25 | impl From for CongestionControlAlgorithm { 26 | fn from(item: quiche::CongestionControlAlgorithm) -> Self { 27 | match item { 28 | quiche::CongestionControlAlgorithm::Reno => CongestionControlAlgorithm::Reno, 29 | quiche::CongestionControlAlgorithm::CUBIC => CongestionControlAlgorithm::CUBIC, 30 | quiche::CongestionControlAlgorithm::BBR => CongestionControlAlgorithm::BBR, 31 | } 32 | } 33 | } 34 | 35 | #[napi] 36 | impl Config { 37 | 38 | #[napi(constructor)] 39 | pub fn new() -> Result { 40 | let config = quiche::Config::new( 41 | quiche::PROTOCOL_VERSION 42 | ).or_else( 43 | |err| Err(napi::Error::from_reason(err.to_string())) 44 | )?; 45 | return Ok(Config(config)); 46 | } 47 | 48 | /// Creates configuration with custom TLS context 49 | /// Servers must be setup with a key and cert 50 | #[napi(factory)] 51 | pub fn with_boring_ssl_ctx( 52 | verify_peer: bool, 53 | verify_allow_fail: bool, 54 | ca: Option, 55 | key: Option>, 56 | cert: Option>, 57 | sigalgs: Option, 58 | ) -> Result { 59 | let mut ssl_ctx_builder = boring::ssl::SslContextBuilder::new( 60 | boring::ssl::SslMethod::tls(), 61 | ).or_else( 62 | |e| Err(napi::Error::from_reason(e.to_string())) 63 | )?; 64 | let verify_value = if verify_peer { 65 | boring::ssl::SslVerifyMode::PEER | boring::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT 66 | } else { 67 | boring::ssl::SslVerifyMode::NONE 68 | }; 69 | ssl_ctx_builder.set_verify_callback(verify_value, move |pre_verify, _| { 70 | // Override any validation errors, this is needed so we can request certs but validate them 71 | // manually. It's essentially allowing insecure certificates 72 | if verify_allow_fail { 73 | true 74 | } else { 75 | pre_verify 76 | } 77 | }); 78 | // Setup all CA certificates 79 | if let Some(ca) = ca { 80 | let mut x509_store_builder = boring::x509::store::X509StoreBuilder::new() 81 | .or_else( 82 | |e| Err(napi::Error::from_reason(e.to_string())) 83 | )?; 84 | let x509_certs = boring::x509::X509::stack_from_pem( 85 | &ca.to_vec() 86 | ).or_else( 87 | |e| Err(napi::Error::from_reason(e.to_string())) 88 | )?; 89 | for x509 in x509_certs.into_iter() { 90 | x509_store_builder.add_cert(x509) 91 | .or_else( 92 | |e| Err(napi::Error::from_reason(e.to_string())) 93 | )?; 94 | } 95 | let x509_store = x509_store_builder.build(); 96 | ssl_ctx_builder.set_verify_cert_store(x509_store) 97 | .or_else( 98 | |e| Err(napi::Error::from_reason(e.to_string())) 99 | )?; 100 | } 101 | // Setup all certificates and keys 102 | if let (Some(key), Some(cert)) = (key, cert) { 103 | // Right now the boring crate does not provide a straight forward way of 104 | // setting multiple independent certificate chains. So we are just picking 105 | // the first key and cert pair. 106 | let (k, c) = (key[0].to_vec(), cert[0].to_vec()); 107 | let private_key = boring::pkey::PKey::private_key_from_pem(&k) 108 | .or_else( 109 | |err| Err(Error::from_reason(err.to_string())) 110 | )?; 111 | ssl_ctx_builder.set_private_key(&private_key).or_else( 112 | |e| Err(napi::Error::from_reason(e.to_string())) 113 | )?; 114 | let x509_cert_chain = boring::x509::X509::stack_from_pem( 115 | &c 116 | ).or_else( 117 | |err| Err(napi::Error::from_reason(err.to_string())) 118 | )?; 119 | for (i, cert) in x509_cert_chain.iter().enumerate() { 120 | if i == 0 { 121 | ssl_ctx_builder.set_certificate(cert,).or_else( 122 | |err| Err(Error::from_reason(err.to_string())) 123 | )?; 124 | } else { 125 | ssl_ctx_builder.add_extra_chain_cert( 126 | cert.clone(), 127 | ).or_else( 128 | |err| Err(Error::from_reason(err.to_string())) 129 | )?; 130 | } 131 | } 132 | } 133 | // Setup supported signature algorithms 134 | if let Some(sigalgs) = sigalgs { 135 | ssl_ctx_builder.set_sigalgs_list(&sigalgs).or_else( 136 | |e| Err(napi::Error::from_reason(e.to_string())) 137 | )?; 138 | } 139 | let config = quiche::Config::with_boring_ssl_ctx_builder( 140 | quiche::PROTOCOL_VERSION, 141 | ssl_ctx_builder, 142 | ).or_else( 143 | |e| Err(Error::from_reason(e.to_string())) 144 | )?; 145 | return Ok(Config(config)); 146 | } 147 | 148 | #[napi] 149 | pub fn load_cert_chain_from_pem_file(&mut self, file: String) -> Result<()> { 150 | return self.0.load_cert_chain_from_pem_file(&file).or_else( 151 | |err| Err(Error::from_reason(err.to_string())) 152 | ); 153 | } 154 | 155 | #[napi] 156 | pub fn load_priv_key_from_pem_file(&mut self, file: String) -> Result<()> { 157 | return self.0.load_priv_key_from_pem_file(&file).or_else( 158 | |err| Err(Error::from_reason(err.to_string())) 159 | ); 160 | } 161 | 162 | #[napi] 163 | pub fn load_verify_locations_from_file(&mut self, file: String) -> Result<()> { 164 | return self.0.load_verify_locations_from_file(&file).or_else( 165 | |err| Err(Error::from_reason(err.to_string())) 166 | ); 167 | } 168 | 169 | #[napi] 170 | pub fn load_verify_locations_from_directory(&mut self, dir: String) -> Result<()> { 171 | return self.0.load_verify_locations_from_directory(&dir).or_else( 172 | |err| Err(Error::from_reason(err.to_string())) 173 | ); 174 | } 175 | 176 | #[napi] 177 | pub fn verify_peer(&mut self, verify: bool) -> () { 178 | return self.0.verify_peer(verify); 179 | } 180 | 181 | #[napi] 182 | pub fn grease(&mut self, grease: bool) -> () { 183 | return self.0.grease(grease); 184 | } 185 | 186 | #[napi] 187 | pub fn log_keys(&mut self) -> () { 188 | return self.0.log_keys(); 189 | } 190 | 191 | #[napi] 192 | pub fn set_ticket_key(&mut self, key: Uint8Array) -> Result<()> { 193 | return self.0.set_ticket_key(&key).or_else( 194 | |err| Err(Error::from_reason(err.to_string())) 195 | ); 196 | } 197 | 198 | #[napi] 199 | pub fn enable_early_data(&mut self) -> () { 200 | return self.0.enable_early_data(); 201 | } 202 | 203 | #[napi] 204 | pub fn set_application_protos( 205 | &mut self, 206 | protos_list: Vec, 207 | ) -> Result<()> { 208 | let protos_list = protos_list.iter().map( 209 | |proto| proto.as_bytes() 210 | ).collect::>(); 211 | return self.0.set_application_protos(&protos_list).or_else( 212 | |err| Err(Error::from_reason(err.to_string())) 213 | ); 214 | } 215 | 216 | #[napi] 217 | pub fn set_application_protos_wire_format( 218 | &mut self, 219 | protos: Uint8Array 220 | ) -> Result<()> { 221 | return self.0.set_application_protos_wire_format(&protos).or_else( 222 | |err| Err(Error::from_reason(err.to_string())) 223 | ); 224 | } 225 | 226 | #[napi] 227 | pub fn set_max_idle_timeout(&mut self, timeout: i64) -> () { 228 | self.0.set_max_idle_timeout(timeout as u64); 229 | } 230 | 231 | #[napi] 232 | pub fn set_max_recv_udp_payload_size(&mut self, size: i64) -> () { 233 | return self.0.set_max_recv_udp_payload_size( 234 | size as usize 235 | ); 236 | } 237 | 238 | #[napi] 239 | pub fn set_max_send_udp_payload_size(&mut self, size: i64) -> () { 240 | return self.0.set_max_send_udp_payload_size( 241 | size as usize 242 | ); 243 | } 244 | 245 | #[napi] 246 | pub fn set_initial_max_data(&mut self, v: i64) -> () { 247 | return self.0.set_initial_max_data(v as u64); 248 | } 249 | 250 | #[napi] 251 | pub fn set_initial_max_stream_data_bidi_local(&mut self, v: i64) -> () { 252 | return self.0.set_initial_max_stream_data_bidi_local(v as u64); 253 | } 254 | 255 | #[napi] 256 | pub fn set_initial_max_stream_data_bidi_remote(&mut self, v: i64) -> () { 257 | return self.0.set_initial_max_stream_data_bidi_remote(v as u64); 258 | } 259 | 260 | #[napi] 261 | pub fn set_initial_max_stream_data_uni(&mut self, v: i64) -> () { 262 | return self.0.set_initial_max_stream_data_uni(v as u64); 263 | } 264 | 265 | #[napi] 266 | pub fn set_initial_max_streams_bidi(&mut self, v: i64) -> () { 267 | return self.0.set_initial_max_streams_bidi(v as u64); 268 | } 269 | 270 | #[napi] 271 | pub fn set_initial_max_streams_uni( 272 | &mut self, 273 | v: i64 274 | ) -> () { 275 | return self.0.set_initial_max_streams_uni( 276 | v as u64 277 | ); 278 | } 279 | 280 | #[napi] 281 | pub fn set_ack_delay_exponent(&mut self, v: i64) -> () { 282 | return self.0.set_ack_delay_exponent( 283 | v as u64 284 | ); 285 | } 286 | 287 | #[napi] 288 | pub fn set_max_ack_delay(&mut self, v: i64) -> () { 289 | return self.0.set_max_ack_delay( 290 | v as u64 291 | ); 292 | } 293 | 294 | #[napi] 295 | pub fn set_active_connection_id_limit( 296 | &mut self, 297 | v: i64 298 | ) -> () { 299 | return self.0.set_active_connection_id_limit(v as u64); 300 | } 301 | 302 | #[napi] 303 | pub fn set_disable_active_migration(&mut self, v: bool) -> () { 304 | return self.0.set_disable_active_migration(v); 305 | } 306 | 307 | #[napi] 308 | pub fn set_cc_algorithm_name(&mut self, name: String) -> Result<()> { 309 | return self.0.set_cc_algorithm_name(&name).or_else( 310 | |err| Err(Error::from_reason(err.to_string())) 311 | ); 312 | } 313 | 314 | #[napi] 315 | pub fn set_cc_algorithm(&mut self, algo: CongestionControlAlgorithm) -> () { 316 | return self.0.set_cc_algorithm(algo.into()); 317 | } 318 | 319 | #[napi] 320 | pub fn enable_hystart(&mut self, v: bool) { 321 | return self.0.enable_hystart(v); 322 | } 323 | 324 | #[napi] 325 | pub fn enable_pacing(&mut self, v: bool) { 326 | return self.0.enable_pacing(v); 327 | } 328 | 329 | #[napi] 330 | pub fn enable_dgram( 331 | &mut self, 332 | enabled: bool, 333 | recv_queue_len: i64, 334 | send_queue_len: i64, 335 | ) -> () { 336 | return self.0.enable_dgram( 337 | enabled, 338 | recv_queue_len as usize, 339 | send_queue_len as usize 340 | ); 341 | } 342 | 343 | #[napi] 344 | pub fn set_max_stream_window(&mut self, v: i64) { 345 | return self.0.set_max_stream_window(v as u64); 346 | } 347 | 348 | #[napi] 349 | pub fn set_max_connection_window(&mut self, v: i64) -> () { 350 | return self.0.set_max_connection_window(v as u64); 351 | } 352 | 353 | #[napi] 354 | pub fn set_stateless_reset_token(&mut self, v: Option) -> () { 355 | return self.0.set_stateless_reset_token( 356 | v.map(|v| v.get_u128().1) 357 | ); 358 | } 359 | 360 | #[napi] 361 | pub fn set_disable_dcid_reuse(&mut self, v: bool) -> () { 362 | return self.0.set_disable_dcid_reuse(v); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/native/napi/constants.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use napi_derive::napi; 3 | 4 | #[napi] 5 | pub const MAX_CONN_ID_LEN: i64 = quiche::MAX_CONN_ID_LEN as i64; 6 | 7 | #[napi] 8 | pub const MIN_CLIENT_INITIAL_LEN: i64 = quiche::MIN_CLIENT_INITIAL_LEN as i64; 9 | 10 | #[napi] 11 | pub const PROTOCOL_VERSION: u32 = quiche::PROTOCOL_VERSION; 12 | 13 | /// This maximum datagram size to SEND to the UDP socket 14 | /// It must be used with `config.set_max_recv_udp_payload_size` and such 15 | /// But on the receiving side, we actually use the maximum which is 65535 16 | #[napi] 17 | pub const MAX_DATAGRAM_SIZE: i64 = 1350; 18 | 19 | /// This is the maximum size of the packet to be received from the socket 20 | /// This is what you use to receive packets on the UDP socket 21 | /// And you send it to the connection as well 22 | #[napi] 23 | pub const MAX_UDP_PACKET_SIZE: i64 = 65535; 24 | 25 | /// The maximum size of the receiver connection flow control window. 26 | /// Note that this is not exported by quiche, but it is 24 MiB 27 | /// This is the default fro `set_max_connection_window` 28 | #[napi] 29 | pub const MAX_CONNECTION_WINDOW: i64 = 24 * 1024 * 1024; 30 | 31 | /// The maximum size of the receiver stream flow control window. 32 | /// This is the default for `set_max_stream_window` 33 | /// This is not exported by quiche, but it's 16 MiB 34 | #[napi] 35 | pub const MAX_STREAM_WINDOW: i64 = 16 * 1024 * 1024; 36 | 37 | #[napi] 38 | pub const CRYPTO_ERROR_START: u16 = 0x0100; 39 | 40 | #[napi] 41 | pub const CRYPTO_ERROR_STOP: u16 = 0x01FF; 42 | -------------------------------------------------------------------------------- /src/native/napi/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | use napi_derive::napi; 4 | 5 | mod constants; 6 | mod config; 7 | mod connection; 8 | mod stream; 9 | mod path; 10 | mod packet; 11 | 12 | #[napi] 13 | pub fn version_is_supported(version: u32) -> bool { 14 | return quiche::version_is_supported(version); 15 | } 16 | -------------------------------------------------------------------------------- /src/native/napi/packet.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use napi_derive::napi; 3 | use napi::bindgen_prelude::*; 4 | 5 | 6 | #[napi] 7 | pub enum Type { 8 | Initial, 9 | Retry, 10 | Handshake, 11 | ZeroRTT, 12 | VersionNegotiation, 13 | Short 14 | } 15 | 16 | impl From for quiche::Type { 17 | fn from(ty: Type) -> Self { 18 | match ty { 19 | Type::Initial => quiche::Type::Initial, 20 | Type::Retry => quiche::Type::Retry, 21 | Type::Handshake => quiche::Type::Handshake, 22 | Type::ZeroRTT => quiche::Type::ZeroRTT, 23 | Type::VersionNegotiation => quiche::Type::VersionNegotiation, 24 | Type::Short => quiche::Type::Short, 25 | } 26 | } 27 | } 28 | 29 | impl From for Type { 30 | fn from(item: quiche::Type) -> Self { 31 | match item { 32 | quiche::Type::Initial => Type::Initial, 33 | quiche::Type::Retry => Type::Retry, 34 | quiche::Type::Handshake => Type::Handshake, 35 | quiche::Type::ZeroRTT => Type::ZeroRTT, 36 | quiche::Type::VersionNegotiation => Type::VersionNegotiation, 37 | quiche::Type::Short => Type::Short, 38 | } 39 | } 40 | } 41 | 42 | #[napi] 43 | pub struct Header { 44 | pub ty: Type, 45 | pub version: u32, 46 | pub dcid: Uint8Array, 47 | pub scid: Uint8Array, 48 | pub token: Option, 49 | pub versions: Option>, 50 | } 51 | 52 | impl From> for Header { 53 | fn from(header: quiche::Header) -> Self { 54 | Header { 55 | ty: header.ty.into(), 56 | version: header.version, 57 | dcid: header.dcid.as_ref().into(), 58 | scid: header.scid.as_ref().into(), 59 | token: header.token.map(|token| token.into()), 60 | versions: header.versions.map(|versions| versions.to_vec()), 61 | } 62 | } 63 | } 64 | 65 | #[napi] 66 | impl Header { 67 | #[napi(factory)] 68 | pub fn from_slice(mut data: Uint8Array, dcid_len: i64) -> napi::Result { 69 | return quiche::Header::from_slice( 70 | &mut data, 71 | dcid_len as usize 72 | ).or_else( 73 | |e| Err(Error::from_reason(e.to_string())) 74 | ).map(|header| header.into()); 75 | } 76 | } 77 | 78 | #[napi] 79 | pub fn negotiate_version( 80 | scid: Uint8Array, 81 | dcid: Uint8Array, 82 | mut data: Uint8Array, 83 | ) -> napi::Result { 84 | let scid = quiche::ConnectionId::from_ref(&scid); 85 | let dcid = quiche::ConnectionId::from_ref(&dcid); 86 | return quiche::negotiate_version(&scid, &dcid, &mut data).or_else( 87 | |e| Err(Error::from_reason(e.to_string())) 88 | ).map(|v| v as i64); 89 | } 90 | 91 | #[napi] 92 | pub fn retry( 93 | scid: Uint8Array, 94 | dcid: Uint8Array, 95 | new_scid: Uint8Array, 96 | token: Uint8Array, 97 | version: u32, 98 | mut out: Uint8Array 99 | ) -> napi::Result { 100 | let scid = quiche::ConnectionId::from_ref(&scid); 101 | let dcid = quiche::ConnectionId::from_ref(&dcid); 102 | let new_scid = quiche::ConnectionId::from_ref(&new_scid); 103 | return quiche::retry( 104 | &scid, 105 | &dcid, 106 | &new_scid, 107 | &token, 108 | version, 109 | &mut out 110 | ).or_else( 111 | |e| Err(Error::from_reason(e.to_string())) 112 | ).map(|v| v as i64); 113 | } 114 | -------------------------------------------------------------------------------- /src/native/napi/path.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | use napi::bindgen_prelude::*; 3 | use serde::{Serialize, Deserialize}; 4 | use crate::connection; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | #[serde(tag = "type")] 8 | pub enum PathEvent { 9 | New { local: connection::HostPort, peer: connection::HostPort }, 10 | Validated { local: connection::HostPort, peer: connection::HostPort }, 11 | FailedValidation { local: connection::HostPort, peer: connection::HostPort }, 12 | Closed { local: connection::HostPort, peer: connection::HostPort }, 13 | ReusedSourceConnectionId { 14 | seq: u64, 15 | old: (connection::HostPort, connection::HostPort), 16 | new: (connection::HostPort, connection::HostPort), 17 | }, 18 | PeerMigrated { 19 | old: connection::HostPort, 20 | new: connection::HostPort, 21 | } 22 | } 23 | 24 | impl From for PathEvent { 25 | fn from(path_event: quiche::PathEvent) -> Self { 26 | match path_event { 27 | quiche::PathEvent::New(local, peer) => PathEvent::New { 28 | local: connection::HostPort::from(local), 29 | peer: connection::HostPort::from(peer), 30 | }, 31 | quiche::PathEvent::Validated(local, peer) => PathEvent::Validated { 32 | local: connection::HostPort::from(local), 33 | peer: connection::HostPort::from(peer), 34 | }, 35 | quiche::PathEvent::FailedValidation(local, peer) => PathEvent::FailedValidation { 36 | local: connection::HostPort::from(local), 37 | peer: connection::HostPort::from(peer), 38 | }, 39 | quiche::PathEvent::Closed(local, peer) => PathEvent::Closed { 40 | local: connection::HostPort::from(local), 41 | peer: connection::HostPort::from(peer), 42 | }, 43 | quiche::PathEvent::ReusedSourceConnectionId(seq, old, new) => PathEvent::ReusedSourceConnectionId { 44 | seq, 45 | old: (connection::HostPort::from(old.0), connection::HostPort::from(old.1)), 46 | new: (connection::HostPort::from(new.0), connection::HostPort::from(new.1)), 47 | }, 48 | quiche::PathEvent::PeerMigrated(old, new) => PathEvent::PeerMigrated { 49 | old: connection::HostPort::from(old), 50 | new: connection::HostPort::from(new), 51 | }, 52 | } 53 | } 54 | } 55 | 56 | // This is an iterator of the host 57 | #[napi(iterator)] 58 | pub struct HostIter(pub (crate) quiche::SocketAddrIter); 59 | 60 | #[napi] 61 | impl Generator for HostIter { 62 | type Yield = connection::HostPort; 63 | type Next = (); 64 | type Return = (); 65 | 66 | fn next(&mut self, _value: Option) -> Option { 67 | return self.0.next().map( 68 | |socket_addr| socket_addr.into() 69 | ); 70 | } 71 | } 72 | 73 | /// Equivalent to quiche::PathStats 74 | #[napi(object)] 75 | pub struct PathStats { 76 | pub local_host: connection::HostPort, 77 | pub peer_host: connection::HostPort, 78 | pub active: bool, 79 | pub recv: i64, 80 | pub sent: i64, 81 | pub lost: i64, 82 | pub retrans: i64, 83 | pub rtt: i64, 84 | pub cwnd: i64, 85 | pub sent_bytes: i64, 86 | pub recv_bytes: i64, 87 | pub lost_bytes: i64, 88 | pub stream_retrans_bytes: i64, 89 | pub pmtu: i64, 90 | pub delivery_rate: i64, 91 | } 92 | 93 | impl From for PathStats { 94 | fn from(path_stats: quiche::PathStats) -> Self { 95 | PathStats { 96 | local_host: connection::HostPort::from(path_stats.local_addr), 97 | peer_host: connection::HostPort::from(path_stats.peer_addr), 98 | active: path_stats.active, 99 | recv: path_stats.recv as i64, 100 | sent: path_stats.sent as i64, 101 | lost: path_stats.lost as i64, 102 | retrans: path_stats.retrans as i64, 103 | rtt: path_stats.rtt.as_millis() as i64, 104 | cwnd: path_stats.cwnd as i64, 105 | sent_bytes: path_stats.sent_bytes as i64, 106 | recv_bytes: path_stats.recv_bytes as i64, 107 | lost_bytes: path_stats.lost_bytes as i64, 108 | stream_retrans_bytes: path_stats.stream_retrans_bytes as i64, 109 | pmtu: path_stats.pmtu as i64, 110 | delivery_rate: path_stats.delivery_rate as i64, 111 | } 112 | } 113 | } 114 | 115 | #[napi(iterator)] 116 | pub struct PathStatsIter(pub (crate) Box>); 117 | 118 | #[napi] 119 | impl Generator for PathStatsIter { 120 | type Yield = PathStats; 121 | type Next = (); 122 | type Return = (); 123 | 124 | fn next(&mut self, _value: Option) -> Option { 125 | return self.0.next().map( 126 | |path_stats| path_stats.into() 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/native/napi/stream.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | use napi::bindgen_prelude::{Generator}; 3 | 4 | #[napi(iterator)] 5 | pub struct StreamIter(pub (crate) quiche::StreamIter); 6 | 7 | #[napi] 8 | impl Generator for StreamIter { 9 | type Yield = i64; 10 | type Next = (); 11 | type Return = (); 12 | 13 | fn next(&mut self, _value: Option) -> Option { 14 | return self.0.next().map( 15 | |stream_id| stream_id as i64 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/native/quiche.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JS binding to NAPI quiche dynamic library. 3 | * This code was derived from the auto-generated binding and declaration 4 | * files provided by napi-rs. 5 | */ 6 | import type { 7 | ConnectionErrorCode, 8 | CongestionControlAlgorithm, 9 | Shutdown, 10 | Type, 11 | ConfigConstructor, 12 | ConnectionConstructor, 13 | HeaderConstructor, 14 | } from './types.js'; 15 | import process from 'process'; 16 | import path from 'path'; 17 | import url from 'url'; 18 | import Module from 'node:module'; 19 | 20 | const require = Module.createRequire(import.meta.url); 21 | const dirname = url.fileURLToPath(new URL('.', import.meta.url)); 22 | 23 | interface Quiche { 24 | MAX_CONN_ID_LEN: number; 25 | MIN_CLIENT_INITIAL_LEN: number; 26 | PROTOCOL_VERSION: number; 27 | MAX_DATAGRAM_SIZE: number; 28 | MAX_UDP_PACKET_SIZE: number; 29 | MAX_STREAM_WINDOW: number; 30 | MAX_CONNECTION_WINDOW: number; 31 | CRYPTO_ERROR_START: number; 32 | CRYPTO_ERROR_STOP: number; 33 | CongestionControlAlgorithm: typeof CongestionControlAlgorithm; 34 | Shutdown: typeof Shutdown; 35 | Type: typeof Type; 36 | ConnectionErrorCode: typeof ConnectionErrorCode; 37 | negotiateVersion( 38 | scid: Uint8Array, 39 | dcid: Uint8Array, 40 | data: Uint8Array, 41 | ): number; 42 | retry( 43 | scid: Uint8Array, 44 | dcid: Uint8Array, 45 | newScid: Uint8Array, 46 | token: Uint8Array, 47 | version: number, 48 | out: Uint8Array, 49 | ): number; 50 | versionIsSupported(version: number): boolean; 51 | Config: ConfigConstructor; 52 | Connection: ConnectionConstructor; 53 | Header: HeaderConstructor; 54 | } 55 | 56 | const projectRoot = path.join(dirname, '../../'); 57 | const prebuildPath = path.join(projectRoot, 'prebuild'); 58 | 59 | /** 60 | * Try require on all prebuild targets first, then 61 | * try require on all npm targets second. 62 | */ 63 | function requireBinding(targets: Array): Quiche { 64 | const prebuildTargets = targets.map((target) => 65 | path.join(prebuildPath, `quic-${target}.node`), 66 | ); 67 | for (const prebuildTarget of prebuildTargets) { 68 | try { 69 | return require(prebuildTarget); 70 | } catch (e) { 71 | if (e.code !== 'MODULE_NOT_FOUND') throw e; 72 | } 73 | try { 74 | return require(url.pathToFileURL(prebuildTarget).href); 75 | } catch (e) { 76 | if (e.code !== 'MODULE_NOT_FOUND') throw e; 77 | } 78 | } 79 | const npmTargets = targets.map((target) => `@matrixai/quic-${target}`); 80 | for (const npmTarget of npmTargets) { 81 | try { 82 | return require(npmTarget); 83 | } catch (e) { 84 | if (e.code !== 'MODULE_NOT_FOUND') throw e; 85 | } 86 | try { 87 | return require(url.pathToFileURL(npmTarget).href); 88 | } catch (e) { 89 | if (e.code !== 'MODULE_NOT_FOUND') throw e; 90 | } 91 | } 92 | throw new Error( 93 | `Failed requiring possible native bindings: ${prebuildTargets.concat( 94 | npmTargets, 95 | )}`, 96 | ); 97 | } 98 | 99 | let nativeBinding: Quiche; 100 | 101 | /** 102 | * For desktop we only support win32, darwin and linux. 103 | * Mobile OS support is pending. 104 | */ 105 | switch (process.platform) { 106 | case 'win32': 107 | switch (process.arch) { 108 | case 'x64': 109 | nativeBinding = requireBinding(['win32-x64']); 110 | break; 111 | case 'ia32': 112 | nativeBinding = requireBinding(['win32-ia32']); 113 | break; 114 | case 'arm64': 115 | nativeBinding = requireBinding(['win32-arm64']); 116 | break; 117 | default: 118 | throw new Error(`Unsupported architecture on Windows: ${process.arch}`); 119 | } 120 | break; 121 | case 'darwin': 122 | switch (process.arch) { 123 | case 'x64': 124 | nativeBinding = requireBinding([ 125 | 'darwin-x64', 126 | 'darwin-x64+arm64', 127 | 'darwin-arm64+x64', 128 | 'darwin-universal', 129 | ]); 130 | break; 131 | case 'arm64': 132 | nativeBinding = requireBinding([ 133 | 'darwin-arm64', 134 | 'darwin-arm64+x64', 135 | 'darwin-x64+arm64', 136 | 'darwin-universal', 137 | ]); 138 | break; 139 | default: 140 | throw new Error(`Unsupported architecture on macOS: ${process.arch}`); 141 | } 142 | break; 143 | case 'linux': 144 | switch (process.arch) { 145 | case 'x64': 146 | nativeBinding = requireBinding(['linux-x64']); 147 | break; 148 | case 'arm64': 149 | nativeBinding = requireBinding(['linux-arm64']); 150 | break; 151 | case 'arm': 152 | nativeBinding = requireBinding(['linux-arm']); 153 | break; 154 | default: 155 | throw new Error(`Unsupported architecture on Linux: ${process.arch}`); 156 | } 157 | break; 158 | default: 159 | throw new Error( 160 | `Unsupported OS: ${process.platform}, architecture: ${process.arch}`, 161 | ); 162 | } 163 | 164 | export default nativeBinding; 165 | 166 | export type { Quiche }; 167 | -------------------------------------------------------------------------------- /src/native/types.ts: -------------------------------------------------------------------------------- 1 | import type { Opaque } from '../types.js'; 2 | 3 | type QuicheTimeInstant = Opaque<'QuicheTimeInstant', object>; 4 | 5 | interface Config { 6 | loadCertChainFromPemFile(file: string): void; 7 | loadPrivKeyFromPemFile(file: string): void; 8 | loadVerifyLocationsFromFile(file: string): void; 9 | loadVerifyLocationsFromDirectory(dir: string): void; 10 | verifyPeer(verify: boolean): void; 11 | grease(grease: boolean): void; 12 | logKeys(): void; 13 | setTicketKey(key: Uint8Array): void; 14 | enableEarlyData(): void; 15 | setApplicationProtos(protosList: Array): void; 16 | setApplicationProtosWireFormat(protos: Uint8Array): void; 17 | setMaxIdleTimeout(timeout: number): void; 18 | setMaxRecvUdpPayloadSize(size: number): void; 19 | setMaxSendUdpPayloadSize(size: number): void; 20 | setInitialMaxData(v: number): void; 21 | setInitialMaxStreamDataBidiLocal(v: number): void; 22 | setInitialMaxStreamDataBidiRemote(v: number): void; 23 | setInitialMaxStreamDataUni(v: number): void; 24 | setInitialMaxStreamsBidi(v: number): void; 25 | setInitialMaxStreamsUni(v: number): void; 26 | setAckDelayExponent(v: number): void; 27 | setMaxAckDelay(v: number): void; 28 | setActiveConnectionIdLimit(v: number): void; 29 | setDisableActiveMigration(v: boolean): void; 30 | setCcAlgorithmName(name: string): void; 31 | setCcAlgorithm(algo: CongestionControlAlgorithm): void; 32 | enableHystart(v: boolean): void; 33 | enablePacing(v: boolean): void; 34 | enableDgram( 35 | enabled: boolean, 36 | recvQueueLen: number, 37 | sendQueueLen: number, 38 | ): void; 39 | setMaxStreamWindow(v: number): void; 40 | setMaxConnectionWindow(v: number): void; 41 | setStatelessResetToken(v?: bigint | undefined | null): void; 42 | setDisableDcidReuse(v: boolean): void; 43 | } 44 | 45 | interface ConfigConstructor { 46 | new (): Config; 47 | withBoringSslCtx( 48 | verifyPeer: boolean, 49 | verifyAllowFail: boolean, 50 | ca?: Uint8Array | undefined | null, 51 | key?: Array | undefined | null, 52 | cert?: Array | undefined | null, 53 | sigalgs?: string | undefined | null, 54 | ): Config; 55 | } 56 | 57 | interface Connection { 58 | setKeylog(path: string): void; 59 | setSession(session: Uint8Array): void; 60 | recv(data: Uint8Array, recvInfo: RecvInfo): number; 61 | send(data: Uint8Array): [number, SendInfo] | null; 62 | sendOnPath( 63 | data: Uint8Array, 64 | from?: HostPort | undefined | null, 65 | to?: HostPort | undefined | null, 66 | ): [number, SendInfo] | null; 67 | sendQuantum(): number; 68 | sendQuantumOnPath(localHost: HostPort, peerHost: HostPort): number; 69 | streamRecv(streamId: number, data: Uint8Array): [number, boolean] | null; 70 | streamSend(streamId: number, data: Uint8Array, fin: boolean): number | null; 71 | streamPriority(streamId: number, urgency: number, incremental: boolean): void; 72 | streamShutdown( 73 | streamId: number, 74 | direction: Shutdown, 75 | err: number, 76 | ): void | null; 77 | streamCapacity(streamId: number): number; 78 | streamReadable(streamId: number): boolean; 79 | streamWritable(streamId: number, len: number): boolean; 80 | streamFinished(streamId: number): boolean; 81 | peerStreamsLeftBidi(): number; 82 | peerStreamsLeftUni(): number; 83 | readable(): StreamIter; 84 | writable(): StreamIter; 85 | maxSendUdpPayloadSize(): number; 86 | dgramRecv(data: Uint8Array): number | null; 87 | dgramRecvVec(): Uint8Array | null; 88 | dgramRecvPeek(data: Uint8Array, len: number): number | null; 89 | dgramRecvFrontLen(): number | null; 90 | dgramRecvQueueLen(): number; 91 | dgramRecvQueueByteSize(): number; 92 | dgramSendQueueLen(): number; 93 | dgramSendQueueByteSize(): number; 94 | isDgramSendQueueFull(): boolean; 95 | isDgramRecvQueueFull(): boolean; 96 | dgramSend(data: Uint8Array): void | null; 97 | dgramSendVec(data: Uint8Array): void | null; 98 | dgramPurgeOutgoing(f: (arg0: Uint8Array) => boolean): void; 99 | dgramMaxWritableLen(): number | null; 100 | timeout(): number | null; 101 | onTimeout(): void; 102 | probePath(localHost: HostPort, peerHost: HostPort): number; 103 | migrateSource(localHost: HostPort): number; 104 | migrate(localHost: HostPort, peerHost: HostPort): number; 105 | newSourceCid( 106 | scid: Uint8Array, 107 | resetToken: bigint, 108 | retireIfNeeded: boolean, 109 | ): number; 110 | activeSourceCids(): number; 111 | maxActiveSourceCids(): number; 112 | sourceCidsLeft(): number; 113 | retireDestinationCid(dcidSeq: number): void; 114 | pathEventNext(): PathEvent; 115 | retiredScidNext(): Uint8Array | null; 116 | availableDcids(): number; 117 | pathsIter(from: HostPort): HostIter; 118 | close(app: boolean, err: number, reason: Uint8Array): void | null; 119 | traceId(): string; 120 | applicationProto(): Uint8Array; 121 | serverName(): string | null; 122 | peerCertChain(): Array | null; 123 | session(): Uint8Array | null; 124 | sourceId(): Uint8Array; 125 | destinationId(): Uint8Array; 126 | isEstablished(): boolean; 127 | isResumed(): boolean; 128 | isInEarlyData(): boolean; 129 | isReadable(): boolean; 130 | isPathValidated(from: HostPort, to: HostPort): boolean; 131 | isDraining(): boolean; 132 | isClosed(): boolean; 133 | isTimedOut(): boolean; 134 | peerError(): ConnectionError | null; 135 | localError(): ConnectionError | null; 136 | stats(): Stats; 137 | pathStats(): Array; 138 | sendAckEliciting(): void; 139 | } 140 | 141 | interface ConnectionConstructor { 142 | connect( 143 | serverName: string | undefined | null, 144 | scid: Uint8Array, 145 | localHost: HostPort, 146 | remoteHost: HostPort, 147 | config: Config, 148 | ): Connection; 149 | accept( 150 | scid: Uint8Array, 151 | odcid: Uint8Array | undefined | null, 152 | localHost: HostPort, 153 | remoteHost: HostPort, 154 | config: Config, 155 | ): Connection; 156 | } 157 | 158 | interface Header { 159 | ty: Type; 160 | version: number; 161 | dcid: Uint8Array; 162 | scid: Uint8Array; 163 | token?: Uint8Array; 164 | versions?: Array; 165 | } 166 | 167 | interface HeaderConstructor { 168 | fromSlice(data: Uint8Array, dcidLen: number): Header; 169 | } 170 | 171 | enum CongestionControlAlgorithm { 172 | Reno = 0, 173 | CUBIC = 1, 174 | BBR = 2, 175 | } 176 | 177 | enum Shutdown { 178 | Read = 0, 179 | Write = 1, 180 | } 181 | 182 | enum Type { 183 | Initial = 0, 184 | Retry = 1, 185 | Handshake = 2, 186 | ZeroRTT = 3, 187 | VersionNegotiation = 4, 188 | Short = 5, 189 | } 190 | 191 | /** 192 | * QUIC transport error codes 193 | * https://www.rfc-editor.org/rfc/rfc9000#section-20.1 194 | * Note that `CryptoError` is a range of error codes. 195 | * Therefore it is not featured in this enum. 196 | * You can instead fetch it from the constants. 197 | */ 198 | enum ConnectionErrorCode { 199 | NoError = 0, 200 | InternalError = 1, 201 | ConnectionRefused = 2, 202 | FlowControlError = 3, 203 | StreamLimitError = 4, 204 | StreamStateError = 5, 205 | FinalSizeError = 6, 206 | FrameEncodingError = 7, 207 | TransportParameterError = 8, 208 | ConnectionIdLimitError = 9, 209 | ProtocolViolation = 10, 210 | InvalidToken = 11, 211 | ApplicationError = 12, 212 | CryptoBufferExceeded = 13, 213 | KeyUpdateError = 14, 214 | AEADLimitReached = 15, 215 | NoViablePath = 16, 216 | } 217 | 218 | /** 219 | * CryptoError is a range from `0x100` to `0x01FF`. 220 | * It maps from the TLS `AlertDescription` codes, offset 221 | * by `0x100`. These are known codes of TLS 1.3 hardcoded in 222 | * QUIC RFC 9000. 223 | * See the TLS 1.3 codes in: https://www.rfc-editor.org/rfc/rfc8446#section-6 224 | */ 225 | enum CryptoError { 226 | CloseNotify = 256, 227 | UnexpectedMessage = 266, 228 | BadRecordMac = 276, 229 | RecordOverflow = 278, 230 | HandshakeFailure = 296, 231 | BadCertificate = 298, 232 | UnsupportedCertificate = 299, 233 | CertificateRevoked = 300, 234 | CertificateExpired = 301, 235 | CertificateUnknown = 302, 236 | IllegalParameter = 303, 237 | UnknownCA = 304, 238 | AccessDenied = 305, 239 | DecodeError = 306, 240 | DecryptError = 307, 241 | ProtocolVersion = 326, 242 | InsufficientSecurity = 327, 243 | InternalError = 336, 244 | InappropriateFallback = 342, 245 | UserCanceled = 346, 246 | MissingExtension = 365, 247 | UnsupportedExtension = 366, 248 | UnrecognizedName = 368, 249 | BadCertificateStatusResponse = 369, 250 | UnknownPSKIdentity = 371, 251 | CertificateRequired = 372, 252 | NoApplicationProtocol = 376, 253 | } 254 | 255 | type ConnectionError = { 256 | isApp: boolean; 257 | errorCode: number; 258 | reason: Uint8Array; 259 | }; 260 | 261 | type Stats = { 262 | recv: number; 263 | sent: number; 264 | lost: number; 265 | retrans: number; 266 | sentBytes: number; 267 | recvBytes: number; 268 | lostBytes: number; 269 | streamRetransBytes: number; 270 | pathsCount: number; 271 | peerMaxIdleTimeout: number; 272 | peerMaxUdpPayloadSize: number; 273 | peerInitialMaxData: number; 274 | peerInitialMaxStreamDataBidiLocal: number; 275 | peerInitialMaxStreamDataBidiRemote: number; 276 | peerInitialMaxStreamDataUni: number; 277 | peerInitialMaxStreamsBidi: number; 278 | peerInitialMaxStreamsUni: number; 279 | peerAckDelayExponent: number; 280 | peerMaxAckDelay: number; 281 | peerDisableActiveMigration: boolean; 282 | peerActiveConnIdLimit: number; 283 | peerMaxDatagramFrameSize?: number; 284 | }; 285 | 286 | type HostPort = { 287 | host: string; 288 | port: number; 289 | }; 290 | 291 | type SendInfo = { 292 | /** The local address the packet should be sent from. */ 293 | from: HostPort; 294 | /** The remote address the packet should be sent to. */ 295 | to: HostPort; 296 | /** The time to send the packet out for pacing. */ 297 | at: QuicheTimeInstant; 298 | }; 299 | 300 | type RecvInfo = { 301 | /** The remote address the packet was received from. */ 302 | from: HostPort; 303 | /** The local address the packet was sent to. */ 304 | to: HostPort; 305 | }; 306 | 307 | type PathStats = { 308 | localHost: HostPort; 309 | peerHost: HostPort; 310 | active: boolean; 311 | recv: number; 312 | sent: number; 313 | lost: number; 314 | retrans: number; 315 | rtt: number; 316 | cwnd: number; 317 | sentBytes: number; 318 | recvBytes: number; 319 | lostBytes: number; 320 | streamRetransBytes: number; 321 | pmtu: number; 322 | deliveryRate: number; 323 | }; 324 | 325 | type PathEvent = 326 | | { 327 | type: 'New'; 328 | local: HostPort; 329 | peer: HostPort; 330 | } 331 | | { 332 | type: 'Validated'; 333 | local: HostPort; 334 | peer: HostPort; 335 | } 336 | | { 337 | type: 'Closed'; 338 | local: HostPort; 339 | peer: HostPort; 340 | } 341 | | { 342 | type: 'ReusedSourceConnectionId'; 343 | seq: number; 344 | old: [HostPort, HostPort]; 345 | new: [HostPort, HostPort]; 346 | } 347 | | { 348 | type: 'PeerMigrated'; 349 | old: HostPort; 350 | new: HostPort; 351 | }; 352 | 353 | type StreamIter = { 354 | [Symbol.iterator](): Iterator; 355 | }; 356 | 357 | type HostIter = { 358 | [Symbol.iterator](): Iterator; 359 | }; 360 | 361 | type PathStatsIter = { 362 | [Symbol.iterator](): Iterator; 363 | }; 364 | 365 | export { 366 | CongestionControlAlgorithm, 367 | Shutdown, 368 | Type, 369 | ConnectionErrorCode, 370 | CryptoError, 371 | }; 372 | 373 | export type { 374 | QuicheTimeInstant, 375 | ConnectionError, 376 | Stats, 377 | HostPort as Host, 378 | SendInfo, 379 | RecvInfo, 380 | PathStats, 381 | StreamIter, 382 | HostIter, 383 | PathStatsIter, 384 | PathEvent, 385 | Config, 386 | ConfigConstructor, 387 | Connection, 388 | ConnectionConstructor, 389 | Header, 390 | HeaderConstructor, 391 | }; 392 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type QUICStream from './QUICStream.js'; 2 | import type { CryptoError } from './native/types.js'; 3 | 4 | type POJO = { [key: string]: any }; 5 | 6 | /** 7 | * Opaque types are wrappers of existing types 8 | * that require smart constructors 9 | */ 10 | type Opaque = T & { readonly [brand]: K }; 11 | declare const brand: unique symbol; 12 | 13 | type Class = new (...args: any[]) => T; 14 | 15 | /** 16 | * Generic callback 17 | */ 18 | type Callback

= [], R = any, E extends Error = Error> = { 19 | (e: E, ...params: Partial

): R; 20 | (e?: null | undefined, ...params: P): R; 21 | }; 22 | 23 | /** 24 | * Deconstructed promise 25 | */ 26 | type PromiseDeconstructed = { 27 | p: Promise; 28 | resolveP: (value: T | PromiseLike) => void; 29 | rejectP: (reason?: any) => void; 30 | }; 31 | 32 | /** 33 | * Host is always an IP address 34 | */ 35 | type Host = Opaque<'Host', string>; 36 | 37 | /** 38 | * Hostnames are resolved to IP addresses 39 | */ 40 | type Hostname = Opaque<'Hostname', string>; 41 | 42 | /** 43 | * Ports are numbers from 0 to 65535 44 | */ 45 | type Port = Opaque<'Port', number>; 46 | 47 | /** 48 | * Combination of `:` 49 | */ 50 | type Address = Opaque<'Address', string>; 51 | 52 | type RemoteInfo = { 53 | host: Host; 54 | port: Port; 55 | }; 56 | 57 | /** 58 | * Client crypto utility object 59 | * Remember every Node Buffer is an ArrayBuffer 60 | */ 61 | type ClientCryptoOps = { 62 | randomBytes(data: ArrayBuffer): Promise; 63 | }; 64 | 65 | /** 66 | * Server crypto utility object 67 | * Remember every Node Buffer is an ArrayBuffer 68 | */ 69 | type ServerCryptoOps = { 70 | sign(key: ArrayBuffer, data: ArrayBuffer): Promise; 71 | verify( 72 | key: ArrayBuffer, 73 | data: ArrayBuffer, 74 | sig: ArrayBuffer, 75 | ): Promise; 76 | }; 77 | 78 | type QUICClientCrypto = { 79 | ops: ClientCryptoOps; 80 | }; 81 | 82 | type QUICServerCrypto = { 83 | key: ArrayBuffer; 84 | ops: ServerCryptoOps; 85 | }; 86 | 87 | /** 88 | * Custom hostname resolution. It is expected this returns an IP address. 89 | */ 90 | type ResolveHostname = (hostname: string) => string | PromiseLike; 91 | 92 | /** 93 | * Custom TLS verification callback. 94 | * The peer cert chain will be passed as the first parameter. 95 | * The CA certs will also be available as a second parameter. 96 | * The certs are in DER binary format. 97 | * It will be an empty array if there were no CA certs. 98 | * If it fails, return a `CryptoError` code. 99 | */ 100 | type TLSVerifyCallback = ( 101 | certs: Array, 102 | ca: Array, 103 | ) => PromiseLike; 104 | 105 | type QUICConfig = { 106 | /** 107 | * Certificate authority certificate in PEM format or Uint8Array buffer 108 | * containing PEM formatted certificate. Each string or Uint8Array can be 109 | * one certificate or multiple certificates concatenated together. The order 110 | * does not matter, each is an independent certificate authority. Multiple 111 | * concatenated certificate authorities can be passed. They are all 112 | * concatenated together. 113 | * 114 | * When this is not set, this defaults to the operating system's CA 115 | * certificates. OpenSSL (and forks of OpenSSL) all support the 116 | * environment variables `SSL_CERT_DIR` and `SSL_CERT_FILE`. 117 | */ 118 | ca?: string | Array | Uint8Array | Array; 119 | 120 | /** 121 | * Private key as a PEM string or Uint8Array buffer containing PEM formatted 122 | * key. You can pass multiple keys. The number of keys must match the number 123 | * of certs. Each key must be associated to the corresponding cert chain. 124 | * 125 | * Currently multiple key and certificate chains is not supported. 126 | */ 127 | key?: string | Array | Uint8Array | Array; 128 | 129 | /** 130 | * X.509 certificate chain in PEM format or Uint8Array buffer containing 131 | * PEM formatted certificate chain. Each string or Uint8Array is a 132 | * certificate chain in subject to issuer order. Multiple certificate chains 133 | * can be passed. The number of certificate chains must match the number of 134 | * keys. Each certificate chain must be associated to the corresponding key. 135 | * 136 | * Currently multiple key and certificate chains is not supported. 137 | */ 138 | cert?: string | Array | Uint8Array | Array; 139 | 140 | /** 141 | * Colon separated list of supported signature algorithms. 142 | * 143 | * When this is not set, this defaults to the following list: 144 | * - rsa_pkcs1_sha256 145 | * - rsa_pkcs1_sha384 146 | * - rsa_pkcs1_sha512 147 | * - rsa_pss_rsae_sha256 148 | * - rsa_pss_rsae_sha384 149 | * - rsa_pss_rsae_sha512 150 | * - ecdsa_secp256r1_sha256 151 | * - ecdsa_secp384r1_sha384 152 | * - ecdsa_secp521r1_sha512 153 | * - ed25519 154 | */ 155 | sigalgs?: string; 156 | 157 | /** 158 | * Verify the other peer. 159 | * Clients by default set this to true. 160 | * Servers by default set this to false. 161 | * Servers will not request peer certs unless this is true. 162 | * Server certs are always sent 163 | */ 164 | verifyPeer: boolean; 165 | 166 | /** 167 | * Custom TLS verification callback. 168 | * It is expected that the callback will throw an error if the verification 169 | * fails. 170 | * Will be ignored if `verifyPeer` is false. 171 | */ 172 | verifyCallback?: TLSVerifyCallback; 173 | 174 | /** 175 | * Enables the logging of secret keys to a file path. 176 | * Use this with wireshark to decrypt the QUIC packets for debugging. 177 | * This defaults to undefined. 178 | */ 179 | logKeys?: string; 180 | 181 | /** 182 | * Enable "Generate Random extensions and Sustain Extensibility". 183 | * This prevents protocol ossification by periodically introducing 184 | * random no-op values in the optional fields in TLS. 185 | * This defaults to true. 186 | */ 187 | grease: boolean; 188 | 189 | /** 190 | * This controls the interval for keeping alive an idle connection. 191 | * This time will be used to send a ping frame to keep the connection alive. 192 | * This is only useful if the `maxIdleTimeout` is set to greater than 0. 193 | * This is defaulted to `undefined`. 194 | * This is not a quiche option. 195 | */ 196 | keepAliveIntervalTime?: number; 197 | 198 | /** 199 | * Maximum number of milliseconds to wait for an idle connection. 200 | * If this time is exhausted with no answer from the peer, then 201 | * the connection will time out. This applies to any open connection. 202 | * Note that the QUIC client will repeatedly send initial packets to 203 | * a non-responding QUIC server up to this time. 204 | * This is defaulted to `0` meaning infinite time. 205 | */ 206 | maxIdleTimeout: number; 207 | 208 | /** 209 | * Maximum incoming UDP payload size. 210 | * The maximum IPv4 UDP payload size is 65507. 211 | * The maximum IPv6 UDP payload size is 65527. 212 | * This is defaulted to 65527. 213 | */ 214 | maxRecvUdpPayloadSize: number; 215 | 216 | /** 217 | * Maximum outgoing UDP payload size. 218 | * 219 | * It is advantageous to set this size to be lower than the maximum 220 | * transmission unit size, which is commonly set to 1500. 221 | * This is defaulted 1200. It is also the minimum. 222 | */ 223 | maxSendUdpPayloadSize: number; 224 | 225 | /** 226 | * Maximum buffer size of incoming stream data for an entire connection. 227 | * If set to 0, then no incoming stream data is allowed, therefore setting 228 | * to 0 effectively disables incoming stream data. 229 | * This defaults to 10 MiB. 230 | */ 231 | initialMaxData: number; 232 | 233 | /** 234 | * Maximum buffer size of incoming stream data for a locally initiated 235 | * bidirectional stream. This is the buffer size for a single stream. 236 | * If set to 0, this disables incoming stream data for locally initiated 237 | * bidirectional streams. 238 | * This defaults to 1 MiB. 239 | */ 240 | initialMaxStreamDataBidiLocal: number; 241 | 242 | /** 243 | * Maximum buffer size of incoming stream data for a remotely initiated 244 | * bidirectional stream. This is the buffer size for a single stream. 245 | * If set to 0, this disables incoming stream data for remotely initiated 246 | * bidirectional streams. 247 | * This defaults to 1 MiB. 248 | */ 249 | initialMaxStreamDataBidiRemote: number; 250 | 251 | /** 252 | * Maximum buffer size of incoming stream data for a remotely initiated 253 | * unidirectional stream. This is the buffer size for a single stream. 254 | * If set to 0, this disables incoming stream data for remotely initiated 255 | * unidirectional streams. 256 | * This defaults to 1 MiB. 257 | */ 258 | initialMaxStreamDataUni: number; 259 | 260 | /** 261 | * Maximum number of remotely initiated bidirectional streams. 262 | * A bidirectional stream is closed once all incoming data is read up to the 263 | * fin offset or when the stream's read direction is shutdown and all 264 | * outgoing data is acked by the peer up to the fin offset or when the 265 | * stream's write direction is shutdown. 266 | * This defaults to 100. 267 | */ 268 | initialMaxStreamsBidi: number; 269 | 270 | /** 271 | * Maximum number of remotely initiated unidirectional streams. 272 | * A unidirectional stream is closed once all incoming data is read up to the 273 | * fin offset or when the stream's read direction is shutdown. 274 | * This defaults to 100. 275 | */ 276 | initialMaxStreamsUni: number; 277 | 278 | /** 279 | * This defaults to 24 MiB. 280 | */ 281 | maxConnectionWindow: number; 282 | 283 | /** 284 | * This defaults to 16 MiB. 285 | */ 286 | maxStreamWindow: number; 287 | 288 | /** 289 | * Enables receiving dgram. 290 | * The 2 numbers are receive queue length and send queue length. 291 | * This defaults to `[false, 0, 0]`. 292 | */ 293 | enableDgram: [boolean, number, number]; 294 | 295 | disableActiveMigration: boolean; 296 | 297 | /** 298 | * Application protocols is necessary for ALPN. 299 | * This is must be non-empty, otherwise there will be a 300 | * `NO_APPLICATION_PROTOCOL` error. 301 | * Choose from: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids 302 | * For HTTP3, use `['h3', 'h3-29', 'h3-28', 'h3-27']`. 303 | * Both the client and server must share the ALPN in order to establish a 304 | * connection. 305 | * This defaults to `['quic']` as a placeholder ALPN. 306 | */ 307 | applicationProtos: string[]; 308 | 309 | enableEarlyData: boolean; 310 | 311 | /** 312 | * Defines the size of the Buffer used to read out data from the readable stream. 313 | * This affects amount of memory reserved by the stream. 314 | */ 315 | readableChunkSize: number; 316 | }; 317 | 318 | type QUICClientConfigInput = Partial; 319 | 320 | type QUICServerConfigInput = Partial & { 321 | key: string | Array | Uint8Array | Array; 322 | cert: string | Array | Uint8Array | Array; 323 | }; 324 | 325 | type ConnectionId = Opaque<'ConnectionId', Buffer>; 326 | 327 | type ConnectionIdString = Opaque<'ConnectionIdString', string>; 328 | 329 | type ConnectionMetadata = { 330 | localHost: string; 331 | localPort: number; 332 | remoteHost: string; 333 | remotePort: number; 334 | localCertsChain: Array; 335 | localCACertsChain: Array; 336 | remoteCertsChain: Array; 337 | }; 338 | 339 | type StreamId = Opaque<'StreamId', number>; 340 | 341 | /** 342 | * Maps reason (most likely an exception) to a stream code. 343 | * Use `0` to indicate unknown/default reason. 344 | */ 345 | type StreamReasonToCode = (type: 'read' | 'write', reason?: any) => number; 346 | 347 | /** 348 | * Maps code to a reason. 0 usually indicates unknown/default reason. 349 | */ 350 | type StreamCodeToReason = (type: 'read' | 'write', code: number) => any; 351 | 352 | type QUICStreamMap = Map; 353 | 354 | export type { 355 | POJO, 356 | Opaque, 357 | Class, 358 | Callback, 359 | PromiseDeconstructed, 360 | Host, 361 | Hostname, 362 | Port, 363 | Address, 364 | RemoteInfo, 365 | ClientCryptoOps, 366 | ServerCryptoOps, 367 | QUICClientCrypto, 368 | QUICServerCrypto, 369 | ResolveHostname, 370 | TLSVerifyCallback, 371 | QUICConfig, 372 | QUICClientConfigInput, 373 | QUICServerConfigInput, 374 | ConnectionId, 375 | ConnectionIdString, 376 | ConnectionMetadata, 377 | StreamId, 378 | StreamReasonToCode, 379 | StreamCodeToReason, 380 | QUICStreamMap, 381 | }; 382 | -------------------------------------------------------------------------------- /tests/QUICConnectionId.test.ts: -------------------------------------------------------------------------------- 1 | import * as testsUtils from './utils.js'; 2 | import QUICConnectionId from '#QUICConnectionId.js'; 3 | import quiche from '#native/quiche.js'; 4 | 5 | describe(QUICConnectionId.name, () => { 6 | test('connection ID is a Uint8Array', async () => { 7 | const cidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); 8 | await testsUtils.randomBytes(cidBuffer); 9 | const cid = new QUICConnectionId(cidBuffer); 10 | expect(cid).toBeInstanceOf(Uint8Array); 11 | }); 12 | test('connection ID encode to hex string and decode from hex string', async () => { 13 | const cidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); 14 | await testsUtils.randomBytes(cidBuffer); 15 | const cid = new QUICConnectionId(cidBuffer); 16 | const cidString = cid.toString(); 17 | const cid_ = QUICConnectionId.fromString(cidString); 18 | expect(cid).toEqual(cid_); 19 | }); 20 | test('connection ID to buffer and from buffer is zero-copy', async () => { 21 | const cidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); 22 | await testsUtils.randomBytes(cidBuffer); 23 | const cid = new QUICConnectionId(cidBuffer); 24 | expect(cid.toBuffer().buffer).toBe(cidBuffer); 25 | const cid_ = QUICConnectionId.fromBuffer(cid.toBuffer()); 26 | expect(cid_.buffer).toBe(cidBuffer); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/config.test.ts: -------------------------------------------------------------------------------- 1 | import type { X509Certificate } from '@peculiar/x509'; 2 | import * as testsUtils from './utils.js'; 3 | import { clientDefault, serverDefault, buildQuicheConfig } from '#config.js'; 4 | import * as errors from '#errors.js'; 5 | 6 | describe('config', () => { 7 | let keyPairRSA: { 8 | publicKey: JsonWebKey; 9 | privateKey: JsonWebKey; 10 | }; 11 | let certRSA: X509Certificate; 12 | let keyPairRSAPEM: { 13 | publicKey: string; 14 | privateKey: string; 15 | }; 16 | let certRSAPEM: string; 17 | let keyPairECDSA: { 18 | publicKey: JsonWebKey; 19 | privateKey: JsonWebKey; 20 | }; 21 | let certECDSA: X509Certificate; 22 | let keyPairECDSAPEM: { 23 | publicKey: string; 24 | privateKey: string; 25 | }; 26 | let certECDSAPEM: string; 27 | let keyPairEd25519: { 28 | publicKey: JsonWebKey; 29 | privateKey: JsonWebKey; 30 | }; 31 | let certEd25519: X509Certificate; 32 | let keyPairEd25519PEM: { 33 | publicKey: string; 34 | privateKey: string; 35 | }; 36 | let certEd25519PEM: string; 37 | beforeAll(async () => { 38 | keyPairRSA = await testsUtils.generateKeyPairRSA(); 39 | certRSA = await testsUtils.generateCertificate({ 40 | certId: '0', 41 | subjectKeyPair: keyPairRSA, 42 | issuerPrivateKey: keyPairRSA.privateKey, 43 | duration: 60 * 60 * 24 * 365 * 10, 44 | }); 45 | keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); 46 | certRSAPEM = testsUtils.certToPEM(certRSA); 47 | keyPairECDSA = await testsUtils.generateKeyPairECDSA(); 48 | certECDSA = await testsUtils.generateCertificate({ 49 | certId: '0', 50 | subjectKeyPair: keyPairECDSA, 51 | issuerPrivateKey: keyPairECDSA.privateKey, 52 | duration: 60 * 60 * 24 * 365 * 10, 53 | }); 54 | keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); 55 | certECDSAPEM = testsUtils.certToPEM(certECDSA); 56 | keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); 57 | certEd25519 = await testsUtils.generateCertificate({ 58 | certId: '0', 59 | subjectKeyPair: keyPairEd25519, 60 | issuerPrivateKey: keyPairEd25519.privateKey, 61 | duration: 60 * 60 * 24 * 365 * 10, 62 | }); 63 | keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); 64 | certEd25519PEM = testsUtils.certToPEM(certEd25519); 65 | }); 66 | test('build default client config', () => { 67 | const config = buildQuicheConfig(clientDefault); 68 | expect(config).toBeDefined(); 69 | }); 70 | test('build default server config', () => { 71 | const config = buildQuicheConfig(serverDefault); 72 | expect(config).toBeDefined(); 73 | }); 74 | test('build with incorrect configuration', () => { 75 | expect(() => 76 | buildQuicheConfig({ 77 | ...serverDefault, 78 | sigalgs: 'ed448', 79 | }), 80 | ).toThrow(errors.ErrorQUICConfig); 81 | expect(() => 82 | buildQuicheConfig({ 83 | ...serverDefault, 84 | key: [keyPairRSAPEM.privateKey, keyPairECDSAPEM.privateKey], 85 | cert: [certRSAPEM], 86 | }), 87 | ).toThrow(errors.ErrorQUICConfig); 88 | }); 89 | test('build with self-signed certificates', () => { 90 | buildQuicheConfig({ 91 | ...clientDefault, 92 | key: keyPairRSAPEM.privateKey, 93 | cert: certRSAPEM, 94 | }); 95 | buildQuicheConfig({ 96 | ...clientDefault, 97 | key: keyPairECDSAPEM.privateKey, 98 | cert: certECDSAPEM, 99 | }); 100 | buildQuicheConfig({ 101 | ...clientDefault, 102 | key: keyPairEd25519PEM.privateKey, 103 | cert: certEd25519PEM, 104 | }); 105 | buildQuicheConfig({ 106 | ...serverDefault, 107 | key: keyPairRSAPEM.privateKey, 108 | cert: certRSAPEM, 109 | }); 110 | buildQuicheConfig({ 111 | ...serverDefault, 112 | key: keyPairECDSAPEM.privateKey, 113 | cert: certECDSAPEM, 114 | }); 115 | buildQuicheConfig({ 116 | ...serverDefault, 117 | key: keyPairEd25519PEM.privateKey, 118 | cert: certEd25519PEM, 119 | }); 120 | }); 121 | test('build with issued certificates', async () => { 122 | const keyPairParent = await testsUtils.generateKeyPairEd25519(); 123 | const certParent = await testsUtils.generateCertificate({ 124 | certId: '0', 125 | subjectKeyPair: keyPairParent, 126 | issuerPrivateKey: keyPairParent.privateKey, 127 | duration: 60 * 60 * 24 * 365 * 10, 128 | }); 129 | const certParentPEM = testsUtils.certToPEM(certParent); 130 | const keyPairChild = await testsUtils.generateKeyPairECDSA(); 131 | const certChild = await testsUtils.generateCertificate({ 132 | certId: '0', 133 | subjectKeyPair: keyPairChild, 134 | issuerPrivateKey: keyPairParent.privateKey, 135 | duration: 60 * 60 * 24 * 365 * 10, 136 | }); 137 | const keyPairChildPEM = await testsUtils.keyPairECDSAToPEM(keyPairChild); 138 | const certChildPEM = testsUtils.certToPEM(certChild); 139 | buildQuicheConfig({ 140 | ...serverDefault, 141 | ca: certParentPEM, 142 | key: keyPairChildPEM.privateKey, 143 | cert: certChildPEM, 144 | }); 145 | buildQuicheConfig({ 146 | ...clientDefault, 147 | ca: certParentPEM, 148 | key: keyPairChildPEM.privateKey, 149 | cert: certChildPEM, 150 | }); 151 | }); 152 | test('build with multiple certificate authorities', async () => { 153 | const keyPairCA1 = await testsUtils.generateKeyPairRSA(); 154 | const keyPairCA2 = await testsUtils.generateKeyPairECDSA(); 155 | const keyPairCA3 = await testsUtils.generateKeyPairEd25519(); 156 | const caCert1 = await testsUtils.generateCertificate({ 157 | certId: '0', 158 | subjectKeyPair: keyPairCA1, 159 | issuerPrivateKey: keyPairCA1.privateKey, 160 | duration: 60 * 60 * 24 * 365 * 10, 161 | }); 162 | const caCert2 = await testsUtils.generateCertificate({ 163 | certId: '0', 164 | subjectKeyPair: keyPairCA2, 165 | issuerPrivateKey: keyPairCA2.privateKey, 166 | duration: 60 * 60 * 24 * 365 * 10, 167 | }); 168 | const caCert3 = await testsUtils.generateCertificate({ 169 | certId: '0', 170 | subjectKeyPair: keyPairCA3, 171 | issuerPrivateKey: keyPairCA3.privateKey, 172 | duration: 60 * 60 * 24 * 365 * 10, 173 | }); 174 | const caCert1PEM = testsUtils.certToPEM(caCert1); 175 | const caCert2PEM = testsUtils.certToPEM(caCert2); 176 | const caCert3PEM = testsUtils.certToPEM(caCert3); 177 | buildQuicheConfig({ 178 | ...clientDefault, 179 | ca: [caCert1PEM, caCert2PEM, caCert3PEM], 180 | verifyPeer: true, 181 | }); 182 | buildQuicheConfig({ 183 | ...serverDefault, 184 | ca: [caCert1PEM, caCert2PEM, caCert3PEM], 185 | verifyPeer: true, 186 | }); 187 | }); 188 | test('build with certificate chain', async () => { 189 | const keyPairRoot = await testsUtils.generateKeyPairEd25519(); 190 | const certRoot = await testsUtils.generateCertificate({ 191 | certId: '0', 192 | subjectKeyPair: keyPairRoot, 193 | issuerPrivateKey: keyPairRoot.privateKey, 194 | duration: 60 * 60 * 24 * 365 * 10, 195 | }); 196 | const keyPairIntermediate = await testsUtils.generateKeyPairEd25519(); 197 | const certIntermediate = await testsUtils.generateCertificate({ 198 | certId: '0', 199 | subjectKeyPair: keyPairIntermediate, 200 | issuerPrivateKey: keyPairRoot.privateKey, 201 | duration: 60 * 60 * 24 * 365 * 10, 202 | }); 203 | 204 | const keyPairLeaf = await testsUtils.generateKeyPairEd25519(); 205 | const certLeaf = await testsUtils.generateCertificate({ 206 | certId: '0', 207 | subjectKeyPair: keyPairLeaf, 208 | issuerPrivateKey: keyPairIntermediate.privateKey, 209 | duration: 60 * 60 * 24 * 365 * 10, 210 | }); 211 | const certRootPEM = testsUtils.certToPEM(certRoot); 212 | const certIntermediatePEM = testsUtils.certToPEM(certIntermediate); 213 | const certLeafPEM = testsUtils.certToPEM(certLeaf); 214 | // These PEMs already have `\n` at the end 215 | const certChainPEM = [certLeafPEM, certIntermediatePEM].join(''); 216 | const keyPairLeafPEM = await testsUtils.keyPairEd25519ToPEM(keyPairLeaf); 217 | buildQuicheConfig({ 218 | ...clientDefault, 219 | ca: certRootPEM, 220 | key: keyPairLeafPEM.privateKey, 221 | cert: certChainPEM, 222 | }); 223 | buildQuicheConfig({ 224 | ...serverDefault, 225 | ca: certRootPEM, 226 | key: keyPairLeafPEM.privateKey, 227 | cert: certChainPEM, 228 | }); 229 | }); 230 | /** 231 | * This currently is not supported. 232 | * But the test will pass. 233 | */ 234 | test('build with multiple certificate chains', async () => { 235 | const keyPairParent = await testsUtils.generateKeyPairEd25519(); 236 | const certParent = await testsUtils.generateCertificate({ 237 | certId: '0', 238 | subjectKeyPair: keyPairParent, 239 | issuerPrivateKey: keyPairParent.privateKey, 240 | duration: 60 * 60 * 24 * 365 * 10, 241 | }); 242 | const certParentPEM = testsUtils.certToPEM(certParent); 243 | const keyPair1 = await testsUtils.generateKeyPairRSA(); 244 | const keyPairPEM1 = await testsUtils.keyPairRSAToPEM(keyPair1); 245 | const keyPair2 = await testsUtils.generateKeyPairECDSA(); 246 | const keyPairPEM2 = await testsUtils.keyPairECDSAToPEM(keyPair2); 247 | const keyPair3 = await testsUtils.generateKeyPairEd25519(); 248 | const keyPairPEM3 = await testsUtils.keyPairEd25519ToPEM(keyPair3); 249 | const cert1 = await testsUtils.generateCertificate({ 250 | certId: '0', 251 | subjectKeyPair: keyPair1, 252 | issuerPrivateKey: keyPairParent.privateKey, 253 | duration: 60 * 60 * 24 * 365 * 10, 254 | }); 255 | const cert2 = await testsUtils.generateCertificate({ 256 | certId: '0', 257 | subjectKeyPair: keyPair2, 258 | issuerPrivateKey: keyPairParent.privateKey, 259 | duration: 60 * 60 * 24 * 365 * 10, 260 | }); 261 | const cert3 = await testsUtils.generateCertificate({ 262 | certId: '0', 263 | subjectKeyPair: keyPair3, 264 | issuerPrivateKey: keyPairParent.privateKey, 265 | duration: 60 * 60 * 24 * 365 * 10, 266 | }); 267 | const certPEM1 = testsUtils.certToPEM(cert1); 268 | const certPEM2 = testsUtils.certToPEM(cert2); 269 | const certPEM3 = testsUtils.certToPEM(cert3); 270 | buildQuicheConfig({ 271 | ...clientDefault, 272 | ca: certParentPEM, 273 | key: [ 274 | keyPairPEM1.privateKey, 275 | keyPairPEM2.privateKey, 276 | keyPairPEM3.privateKey, 277 | ], 278 | cert: [certPEM1, certPEM2, certPEM3], 279 | verifyPeer: true, 280 | }); 281 | buildQuicheConfig({ 282 | ...serverDefault, 283 | ca: certParentPEM, 284 | key: [ 285 | keyPairPEM1.privateKey, 286 | keyPairPEM2.privateKey, 287 | keyPairPEM3.privateKey, 288 | ], 289 | cert: [certPEM1, certPEM2, certPEM3], 290 | verifyPeer: true, 291 | }); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /tests/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | 3 | /// 4 | 5 | /** 6 | * Follows the globals in jest.config.ts 7 | * @module 8 | */ 9 | declare var projectDir: string; 10 | declare var testDir: string; 11 | declare var defaultTimeout: number; 12 | declare var maxTimeout: number; 13 | -------------------------------------------------------------------------------- /tests/globalSetup.ts: -------------------------------------------------------------------------------- 1 | async function setup() { 2 | // eslint-disable-next-line no-console 3 | console.log('\nGLOBAL SETUP'); 4 | } 5 | 6 | export default setup; 7 | -------------------------------------------------------------------------------- /tests/globalTeardown.ts: -------------------------------------------------------------------------------- 1 | async function teardown() { 2 | // eslint-disable-next-line no-console 3 | console.log('GLOBAL TEARDOWN'); 4 | } 5 | 6 | export default teardown; 7 | -------------------------------------------------------------------------------- /tests/native/quiche.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@fast-check/jest'; 2 | import * as testsUtils from '../utils.js'; 3 | import quiche from '#native/quiche.js'; 4 | 5 | describe('native/quiche', () => { 6 | test.prop([testsUtils.bufferArb({ minLength: 0, maxLength: 100 })])( 7 | 'packet parsing', 8 | (packet) => { 9 | // Remember a UDP payload only has 1 QUIC packet 10 | // But 1 QUIC packet can have multiple QUIC frames 11 | try { 12 | // The `quiche.MAX_CONN_ID_LEN` is 20 bytes 13 | // From 21 bytes it is possible to bypass `BufferTooShort` but it is not guaranteed 14 | // However 20 bytes and under is always `BufferTooShort` 15 | quiche.Header.fromSlice(packet, quiche.MAX_CONN_ID_LEN); 16 | } catch (e) { 17 | expect(e.message).toBe('BufferTooShort'); 18 | // InvalidPacket seems very rare, save it as an example if you find one! 19 | } 20 | }, 21 | ); 22 | test('version negotiation', async () => { 23 | const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); 24 | await testsUtils.randomBytes(scidBuffer); 25 | const scid = new Uint8Array(scidBuffer); 26 | const dcidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); 27 | await testsUtils.randomBytes(dcidBuffer); 28 | const dcid = new Uint8Array(dcidBuffer); 29 | const versionPacket = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); 30 | const versionPacketLength = quiche.negotiateVersion( 31 | scid, 32 | dcid, 33 | versionPacket, 34 | ); 35 | const serverHeaderVersion = quiche.Header.fromSlice( 36 | versionPacket.subarray(0, versionPacketLength), 37 | quiche.MAX_CONN_ID_LEN, 38 | ); 39 | expect(serverHeaderVersion.ty).toBe(quiche.Type.VersionNegotiation); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixAI/js-quic/becf4a722824c71c1a382bfb7c407f81eee380cc/tests/setup.ts -------------------------------------------------------------------------------- /tests/setupAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | 3 | // Default timeout per test 4 | // some tests may take longer in which case you should specify the timeout 5 | // explicitly for each test by using the third parameter of test function 6 | jest.setTimeout(globalThis.defaultTimeout); 7 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import type { Host } from '#types.js'; 2 | import * as utils from '#utils.js'; 3 | 4 | describe('utils', () => { 5 | test('detect IPv4 mapped IPv6 addresses', () => { 6 | expect(utils.isIPv4MappedIPv6('::ffff:127.0.0.1')).toBe(true); 7 | expect(utils.isIPv4MappedIPv6('::ffff:7f00:1')).toBe(true); 8 | expect(utils.isIPv4MappedIPv6('::')).toBe(false); 9 | expect(utils.isIPv4MappedIPv6('::1')).toBe(false); 10 | expect(utils.isIPv4MappedIPv6('127.0.0.1')).toBe(false); 11 | expect(utils.isIPv4MappedIPv6('::ffff:4a7d:2b63')).toBe(true); 12 | expect(utils.isIPv4MappedIPv6('::ffff:7f00:800')).toBe(true); 13 | expect(utils.isIPv4MappedIPv6('::ffff:255.255.255.255')).toBe(true); 14 | }); 15 | test('detect IPv4 mapped IPv6 Dec addresses', () => { 16 | expect(utils.isIPv4MappedIPv6Dec('::ffff:127.0.0.1')).toBe(true); 17 | expect(utils.isIPv4MappedIPv6Dec('::ffff:7f00:1')).toBe(false); 18 | expect(utils.isIPv4MappedIPv6Dec('::')).toBe(false); 19 | expect(utils.isIPv4MappedIPv6Dec('::1')).toBe(false); 20 | expect(utils.isIPv4MappedIPv6Dec('127.0.0.1')).toBe(false); 21 | expect(utils.isIPv4MappedIPv6Dec('::ffff:4a7d:2b63')).toBe(false); 22 | expect(utils.isIPv4MappedIPv6Dec('::ffff:7f00:800')).toBe(false); 23 | expect(utils.isIPv4MappedIPv6Dec('::ffff:255.255.255.255')).toBe(true); 24 | }); 25 | test('detect IPv4 mapped IPv6 Hex addresses', () => { 26 | expect(utils.isIPv4MappedIPv6Hex('::ffff:127.0.0.1')).toBe(false); 27 | expect(utils.isIPv4MappedIPv6Hex('::ffff:7f00:1')).toBe(true); 28 | expect(utils.isIPv4MappedIPv6Hex('::')).toBe(false); 29 | expect(utils.isIPv4MappedIPv6Hex('::1')).toBe(false); 30 | expect(utils.isIPv4MappedIPv6Hex('127.0.0.1')).toBe(false); 31 | expect(utils.isIPv4MappedIPv6Hex('::ffff:4a7d:2b63')).toBe(true); 32 | expect(utils.isIPv4MappedIPv6Hex('::ffff:7f00:800')).toBe(true); 33 | expect(utils.isIPv4MappedIPv6Hex('::ffff:255.255.255.255')).toBe(false); 34 | }); 35 | test('to IPv4 mapped IPv6 addresses Dec', () => { 36 | expect(utils.toIPv4MappedIPv6Dec('127.0.0.1')).toBe('::ffff:127.0.0.1'); 37 | expect(utils.toIPv4MappedIPv6Dec('0.0.0.0')).toBe('::ffff:0.0.0.0'); 38 | expect(utils.toIPv4MappedIPv6Dec('255.255.255.255')).toBe( 39 | '::ffff:255.255.255.255', 40 | ); 41 | expect(utils.toIPv4MappedIPv6Dec('74.125.43.99')).toBe( 42 | '::ffff:74.125.43.99', 43 | ); 44 | }); 45 | test('to IPv4 mapped IPv6 addresses Hex', () => { 46 | expect(utils.toIPv4MappedIPv6Hex('127.0.0.1')).toBe('::ffff:7f00:1'); 47 | expect(utils.toIPv4MappedIPv6Hex('0.0.0.0')).toBe('::ffff:0:0'); 48 | expect(utils.toIPv4MappedIPv6Hex('255.255.255.255')).toBe( 49 | '::ffff:ffff:ffff', 50 | ); 51 | expect(utils.toIPv4MappedIPv6Hex('74.125.43.99')).toBe('::ffff:4a7d:2b63'); 52 | }); 53 | test('from IPv4 mapped IPv6 addresses', () => { 54 | expect(utils.fromIPv4MappedIPv6('::ffff:7f00:1')).toBe('127.0.0.1'); 55 | expect(utils.fromIPv4MappedIPv6('::ffff:127.0.0.1')).toBe('127.0.0.1'); 56 | expect(utils.fromIPv4MappedIPv6('::ffff:0.0.0.0')).toBe('0.0.0.0'); 57 | expect(utils.fromIPv4MappedIPv6('::ffff:255.255.255.255')).toBe( 58 | '255.255.255.255', 59 | ); 60 | expect(utils.fromIPv4MappedIPv6('::ffff:ffff:ffff')).toBe( 61 | '255.255.255.255', 62 | ); 63 | expect(utils.fromIPv4MappedIPv6('::ffff:0:0')).toBe('0.0.0.0'); 64 | expect(utils.fromIPv4MappedIPv6('::ffff:4a7d:2b63')).toBe('74.125.43.99'); 65 | expect(utils.fromIPv4MappedIPv6('::ffff:7f00:800')).toBe('127.0.8.0'); 66 | expect(utils.fromIPv4MappedIPv6('::ffff:800:800')).toBe('8.0.8.0'); 67 | expect(utils.fromIPv4MappedIPv6('::ffff:0:0')).toBe('0.0.0.0'); 68 | // Converting from ::ffff:7f00:1 to ::ffff:127.0.0.1 69 | expect( 70 | utils.toIPv4MappedIPv6Dec(utils.fromIPv4MappedIPv6('::ffff:7f00:1')), 71 | ).toBe('::ffff:127.0.0.1'); 72 | }); 73 | test('to canonical IP address', () => { 74 | // IPv4 -> IPv4 75 | expect(utils.toCanonicalIP('127.0.0.1')).toBe('127.0.0.1'); 76 | expect(utils.toCanonicalIP('0.0.0.0')).toBe('0.0.0.0'); 77 | expect(utils.toCanonicalIP('255.255.255.255')).toBe('255.255.255.255'); 78 | expect(utils.toCanonicalIP('74.125.43.99')).toBe('74.125.43.99'); 79 | // IPv4 mapped hex -> IPv4 80 | expect(utils.toCanonicalIP('::ffff:7f00:1')).toBe('127.0.0.1'); 81 | expect(utils.toCanonicalIP('::ffff:0:0')).toBe('0.0.0.0'); 82 | expect(utils.toCanonicalIP('::ffff:ffff:ffff')).toBe('255.255.255.255'); 83 | expect(utils.toCanonicalIP('::ffff:4a7d:2b63')).toBe('74.125.43.99'); 84 | // IPv4 mapped dec -> IPv4 85 | expect(utils.toCanonicalIP('::ffff:127.0.0.1')).toBe('127.0.0.1'); 86 | expect(utils.toCanonicalIP('::ffff:0.0.0.0')).toBe('0.0.0.0'); 87 | expect(utils.toCanonicalIP('::ffff:255.255.255.255')).toBe( 88 | '255.255.255.255', 89 | ); 90 | expect(utils.toCanonicalIP('::ffff:74.125.43.99')).toBe('74.125.43.99'); 91 | // IPv6 -> IPv6 92 | expect(utils.toCanonicalIP('::1234:7f00:1')).toBe('::1234:7f00:1'); 93 | expect(utils.toCanonicalIP('::1234:0:0')).toBe('::1234:0:0'); 94 | expect(utils.toCanonicalIP('::1234:ffff:ffff')).toBe('::1234:ffff:ffff'); 95 | expect(utils.toCanonicalIP('::1234:4a7d:2b63')).toBe('::1234:4a7d:2b63'); 96 | }); 97 | test('resolves zero IP to local IP', () => { 98 | expect(utils.resolvesZeroIP('0.0.0.0' as Host)).toBe('127.0.0.1'); 99 | expect(utils.resolvesZeroIP('::' as Host)).toBe('::1'); 100 | expect(utils.resolvesZeroIP('::0' as Host)).toBe('::1'); 101 | expect(utils.resolvesZeroIP('::ffff:0.0.0.0' as Host)).toBe( 102 | '::ffff:127.0.0.1', 103 | ); 104 | expect(utils.resolvesZeroIP('::ffff:0:0' as Host)).toBe('::ffff:127.0.0.1'); 105 | // Preserves if not a zero IP 106 | expect(utils.resolvesZeroIP('1.1.1.1' as Host)).toBe('1.1.1.1'); 107 | expect(utils.resolvesZeroIP('::2' as Host)).toBe('::2'); 108 | expect(utils.resolvesZeroIP('::ffff:7f00:1' as Host)).toBe('::ffff:7f00:1'); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "noEmit": false, 6 | "stripInternal": true 7 | }, 8 | "exclude": [ 9 | "./tests/**/*", 10 | "./scripts/**/*", 11 | "./benches/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "tsBuildInfoFile": "./dist/tsbuildinfo", 5 | "incremental": true, 6 | "sourceMap": true, 7 | "declaration": true, 8 | "allowJs": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": false, 11 | "experimentalDecorators": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleResolution": "NodeNext", 17 | "module": "ESNext", 18 | "target": "ES2022", 19 | "baseUrl": "./src", 20 | "paths": { 21 | "#*": ["*"] 22 | }, 23 | "skipLibCheck": true, 24 | "noEmit": true 25 | }, 26 | "include": [ 27 | "./src/**/*", 28 | "./src/**/*.json", 29 | "./tests/**/*", 30 | "./scripts/**/*", 31 | "./benches/**/*" 32 | ], 33 | "ts-node": { 34 | "esm": true, 35 | "transpileOnly": true, 36 | "swc": true 37 | } 38 | } 39 | --------------------------------------------------------------------------------