From 2ffa5ecadd2ac4b85dd2a657c56c3817b4101dc4 Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Fri, 19 May 2023 14:02:28 +1000 Subject: [PATCH 01/22] feat: initial refactor work --- README.md | 55 + jest.config.js | 17 +- package-lock.json | 399 +-- package.json | 23 +- src/QUICClient.ts | 400 ++- src/QUICConnection.ts | 1205 +++++---- src/QUICServer.ts | 183 +- src/QUICSocket.ts | 119 +- src/QUICStream.ts | 28 +- src/config.ts | 216 +- src/errors.ts | 57 +- src/events.ts | 143 +- src/native/napi/config.rs | 113 +- src/native/napi/connection.rs | 9 +- src/native/types.ts | 12 +- src/types.ts | 199 ++ src/utils.ts | 77 + test-bootstrap.ts | 174 ++ test-client-conn.ts | 136 + test-keygen-webcrypto.ts | 443 ++++ test-keygen.ts | 80 + tests/QUICConnectionId.test.ts | 28 + tests/QUICServer.test.ts | 384 +++ tests/config.test.ts | 301 +++ .../quiche.connection.lifecycle.test.ts | 2209 +++++++++++++++++ tests/native/quiche.test.ts | 36 + tests/native/quiche.tls.test.ts | 2019 +++++++++++++++ tests/utils.ts | 518 +++- tsconfig.json | 2 +- 29 files changed, 8373 insertions(+), 1212 deletions(-) create mode 100644 test-bootstrap.ts create mode 100644 test-client-conn.ts create mode 100644 test-keygen-webcrypto.ts create mode 100644 test-keygen.ts create mode 100644 tests/QUICConnectionId.test.ts create mode 100644 tests/QUICServer.test.ts create mode 100644 tests/config.test.ts create mode 100644 tests/native/quiche.connection.lifecycle.test.ts create mode 100644 tests/native/quiche.test.ts create mode 100644 tests/native/quiche.tls.test.ts diff --git a/README.md b/README.md index b1bb2a54..bf637c78 100644 --- a/README.md +++ b/README.md @@ -154,3 +154,58 @@ npm publish --access public git push git push --tags ``` + +--- + +I need to be locked together. + +These need to be atomic operations. + +The only issue is that the "atomicity" is controlled outside of `QUICConnection` atm. + +Whereas it seems to make sense to do this directly? + +``` +recieve +[IF IS DRAINING IS TRUE SKIP SEND] +send +[CLOSE] - we may be "closed here" +set-timeout +``` + +This would be triggered by: +* QUICStream +* keepAliveTimer +* after onTimeout + +``` +send +[CLOSE] - we may be "closed here" +set-timeout +``` + +Remember you may also "receive" and end up closing too. But you will always check if you need to send first before checking the close. At worst it will tell you it's done. + +Now of course we enable calling recv and send. + +But `send` actually ends up calling multiple things here. + +But if `recv` is synchronous, you can always call it infront of `send`. + +This technically means `send` should be encapsulating the logic of setting the timeout. + +If you want to make sure it's re-entrant, you can just "lock" on the send call. + +The setTimeout is then protected. + +The `recv` call is made synchronously. + + + +Receive Send Timer, Send Timer (all of this requires locking the conn lock) + +Closing too, it should require the conn lock +Receive Send [Close] Timer, Send [Close] Timer +It's all optional +It's the send that has to do Send, Close, Timer... that's what needs to check it all +Forget about events for now diff --git a/jest.config.js b/jest.config.js index 90406f0b..58d32f3c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -33,8 +33,21 @@ module.exports = { roots: ['/tests'], testMatch: ['**/?(*.)+(spec|test|unit.test).+(ts|tsx|js|jsx)'], transform: { - '^.+\\.tsx?$': 'ts-jest', - '^.+\\.jsx?$': 'babel-jest', + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + jsc: { + parser: { + syntax: "typescript", + tsx: true, + decorators: compilerOptions.experimentalDecorators, + dynamicImport: true, + }, + target: compilerOptions.target.toLowerCase(), + keepClassNames: true, + }, + } + ], }, reporters: [ 'default', diff --git a/package-lock.json b/package-lock.json index c87574da..a03323b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "0.0.7-alpha.0", "license": "Apache-2.0", "dependencies": { - "@matrixai/async-init": "^1.8.1", - "@matrixai/async-locks": "^3.1.1", - "@matrixai/errors": "^1.1.2", + "@matrixai/async-cancellable": "^1.1.0", + "@matrixai/async-init": "^1.8.3", + "@matrixai/async-locks": "^3.2.0", + "@matrixai/contexts": "^1.0.0", + "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^3.1.0", "@matrixai/resources": "^1.1.3", - "@matrixai/workers": "^1.3.5", + "@matrixai/timer": "^1.1.0", "ip-num": "^1.5.0" }, "bin": { @@ -28,7 +30,8 @@ "@peculiar/asn1-x509": "^2.3.0", "@peculiar/webcrypto": "^1.4.0", "@peculiar/x509": "^1.8.3", - "@swc/core": "^1.2.215", + "@swc/core": "^1.3.62", + "@swc/jest": "^0.2.26", "@types/jest": "^28.1.3", "@types/node": "^18.15.0", "@typescript-eslint/eslint-plugin": "^5.36.2", @@ -43,15 +46,15 @@ "jest-extended": "^3.0.1", "jest-junit": "^14.0.0", "prettier": "^2.6.2", - "rimraf": "^3.0.2", "semver": "^7.3.7", + "shx": "^0.3.4", "sodium-native": "^3.4.1", "systeminformation": "^5.18.5", "ts-jest": "^28.0.5", "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", - "typedoc": "^0.22.15", - "typescript": "^4.5.2" + "typedoc": "^0.23.21", + "typescript": "^4.9.3" }, "optionalDependencies": { "@matrixai/quic-darwin-arm64": "0.0.7-alpha.0", @@ -894,6 +897,43 @@ } } }, + "node_modules/@jest/create-cache-key-function": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz", + "integrity": "sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/create-cache-key-function/node_modules/@types/yargs": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", + "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, "node_modules/@jest/environment": { "version": "28.1.3", "dev": true, @@ -1138,28 +1178,46 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@matrixai/async-cancellable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-cancellable/-/async-cancellable-1.1.0.tgz", + "integrity": "sha512-DxhRAOobxD+bolR93g0jXbpt7X0AeY8ELcT1TgpoQm8jH8KfJxMxguzijozd9Jm1HKcexshiDeBa/COt8p6eJA==" + }, "node_modules/@matrixai/async-init": { - "version": "1.8.2", - "license": "Apache-2.0", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.8.3.tgz", + "integrity": "sha512-AQyffo14v1PpfwevAdJ0sCSl3kMh5KK312s5S3/fgM7AosT5ovtYflWhi1Th9hxGL0mhSu4VwmDwcyeyjsyCgw==", "dependencies": { - "@matrixai/async-locks": "^3.1.2", - "@matrixai/errors": "^1.1.3" + "@matrixai/async-locks": "^3.2.0", + "@matrixai/errors": "^1.1.7" } }, "node_modules/@matrixai/async-locks": { "version": "3.2.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-3.2.0.tgz", + "integrity": "sha512-Gl919y3GK2lBCI7M3MabE2u0+XOhKqqgwFEGVaPSI2BrdSI+RY7K3+dzjTSUTujVZwiYskT611CBvlDm9fhsNg==", "dependencies": { "@matrixai/errors": "^1.1.3", "@matrixai/resources": "^1.1.4", "async-mutex": "^0.3.2" } }, + "node_modules/@matrixai/contexts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@matrixai/contexts/-/contexts-1.0.0.tgz", + "integrity": "sha512-IR8qV5cu8s3QaSrz62kO1sJKJFlEpakXrnG4rErJsPyPulm8NM0nWBYv+4ZvJ/sw64CZ0EziDyCrvWnqY1dUQw==", + "dependencies": { + "@matrixai/async-cancellable": "^1.0.6", + "@matrixai/errors": "^1.1.7", + "@matrixai/timer": "^1.1.0" + } + }, "node_modules/@matrixai/errors": { - "version": "1.1.5", - "license": "Apache-2.0", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.1.7.tgz", + "integrity": "sha512-WD6MrlfgtNSTfXt60lbMgwasS5T7bdRgH4eYSOxV+KWngqlkEij9EoDt5LwdvcMD1yuC33DxPTnH4Xu2XV3nMw==", "dependencies": { - "ts-custom-error": "^3.2.2" + "ts-custom-error": "3.2.2" } }, "node_modules/@matrixai/logger": { @@ -1206,14 +1264,12 @@ "version": "1.1.4", "license": "Apache-2.0" }, - "node_modules/@matrixai/workers": { - "version": "1.3.6", - "license": "Apache-2.0", + "node_modules/@matrixai/timer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@matrixai/timer/-/timer-1.1.0.tgz", + "integrity": "sha512-n9ulMGJhjQyu+fhRxvP5SwY57miOLbRE6gtIzTZD6x7wFL0k5sUBMnEQ6W4QkO1atZnxKMFigg4gt/0V4XlniA==", "dependencies": { - "@matrixai/async-init": "^1.8.2", - "@matrixai/errors": "^1.1.2", - "@matrixai/logger": "^3.0.0", - "threads": "^1.6.5" + "@matrixai/async-cancellable": "^1.0.4" } }, "node_modules/@napi-rs/cli": { @@ -1447,9 +1503,9 @@ } }, "node_modules/@swc/core": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.59.tgz", - "integrity": "sha512-ZBw31zd2E5SXiodwGvjQdx5ZC90b2uyX/i2LeMMs8LKfXD86pfOfQac+JVrnyEKDhASXj9icgsF9NXBhaMr3Kw==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.62.tgz", + "integrity": "sha512-J58hWY+/G8vOr4J6ZH9hLg0lMSijZtqIIf4HofZezGog/pVX6sJyBJ40dZ1ploFkDIlWTWvJyqtpesBKS73gkQ==", "dev": true, "hasInstallScript": true, "engines": { @@ -1460,16 +1516,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.59", - "@swc/core-darwin-x64": "1.3.59", - "@swc/core-linux-arm-gnueabihf": "1.3.59", - "@swc/core-linux-arm64-gnu": "1.3.59", - "@swc/core-linux-arm64-musl": "1.3.59", - "@swc/core-linux-x64-gnu": "1.3.59", - "@swc/core-linux-x64-musl": "1.3.59", - "@swc/core-win32-arm64-msvc": "1.3.59", - "@swc/core-win32-ia32-msvc": "1.3.59", - "@swc/core-win32-x64-msvc": "1.3.59" + "@swc/core-darwin-arm64": "1.3.62", + "@swc/core-darwin-x64": "1.3.62", + "@swc/core-linux-arm-gnueabihf": "1.3.62", + "@swc/core-linux-arm64-gnu": "1.3.62", + "@swc/core-linux-arm64-musl": "1.3.62", + "@swc/core-linux-x64-gnu": "1.3.62", + "@swc/core-linux-x64-musl": "1.3.62", + "@swc/core-win32-arm64-msvc": "1.3.62", + "@swc/core-win32-ia32-msvc": "1.3.62", + "@swc/core-win32-x64-msvc": "1.3.62" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -1481,9 +1537,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.59.tgz", - "integrity": "sha512-AnqWFBgEKHP0jb4iZqx7eVQT9/rX45+DE4Ox7GpwCahUKxxrsDLyXzKhwLwQuAjUvtu5JcSB77szKpPGDM49fQ==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.62.tgz", + "integrity": "sha512-MmGilibITz68LEje6vJlKzc2gUUSgzvB3wGLSjEORikTNeM7P8jXVxE4A8fgZqDeudJUm9HVWrxCV+pHDSwXhA==", "cpu": [ "arm64" ], @@ -1497,9 +1553,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.59.tgz", - "integrity": "sha512-iqDs+yii9mOsmpJez82SEi4d4prWDRlapHxKnDVJ0x1AqRo41vIq8t3fujrvCHYU5VQgOYGh4ooXQpaP2H3B2A==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.62.tgz", + "integrity": "sha512-Xl93MMB3sCWVlYWuQIB+v6EQgzoiuQYK5tNt9lsHoIEVu2zLdkQjae+5FUHZb1VYqCXIiWcULFfVz0R4Sjb7JQ==", "cpu": [ "x64" ], @@ -1513,9 +1569,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.59.tgz", - "integrity": "sha512-PB0PP+SgkCSd/kYmltnPiGv42cOSaih1OjXCEjxvNwUFEmWqluW6uGdWaNiR1LoYMxhcHZTc336jL2+O3l6p0Q==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.62.tgz", + "integrity": "sha512-nJsp6O7kCtAjTTMcIjVB0g5y1JNiYAa5q630eiwrnaHUusEFoANDdORI3Z9vXeikMkng+6yIv9/V8Rb093xLjQ==", "cpu": [ "arm" ], @@ -1529,9 +1585,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.59.tgz", - "integrity": "sha512-Ol/JPszWZ+OZ44FOdJe35TfJ1ckG4pYaisZJ4E7PzfwfVe2ygX85C5WWR4e5L0Y1zFvzpcI7gdyC2wzcXk4Cig==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.62.tgz", + "integrity": "sha512-XGsV93vpUAopDt5y6vPwbK1Nc/MlL55L77bAZUPIiosWD1cWWPHNtNSpriE6+I+JiMHe0pqtfS/SSTk6ZkFQVw==", "cpu": [ "arm64" ], @@ -1545,9 +1601,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.59.tgz", - "integrity": "sha512-PtTTtGbj9GiY5gJdoSFL2A0vL6BRaS1haAhp6g3hZvLDkTTg+rJURmzwBMMjaQlnGC62x/lLf6MoszHG/05//Q==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.62.tgz", + "integrity": "sha512-ESUmJjSlTTkoBy9dMG49opcNn8BmviqStMhwyeD1G8XRnmRVCZZgoBOKdvCXmJhw8bQXDhZumeaTUB+OFUKVXg==", "cpu": [ "arm64" ], @@ -1561,9 +1617,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.59.tgz", - "integrity": "sha512-XBW9AGi0YsIN76IfesnDSBn/5sjR69J75KUNte8sH6seYlHJ0/kblqUMbUcfr0CiGoJadbzAZeKZZmfN7EsHpg==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.62.tgz", + "integrity": "sha512-wnHJkt3ZBrax3SFnUHDcncG6mrSg9ZZjMhQV9Mc3JL1x1s1Gy9rGZCoBNnV/BUZWTemxIBcQbANRSDut/WO+9A==", "cpu": [ "x64" ], @@ -1577,9 +1633,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.59.tgz", - "integrity": "sha512-Cy5E939SdWPQ34cg6UABNO0RyEe0FuWqzZ/GLKtK11Ir4fjttVlucZiY59uQNyUVUc8T2qE0VBFCyD/zYGuHtg==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.62.tgz", + "integrity": "sha512-9oRbuTC/VshB66Rgwi3pTq3sPxSTIb8k9L1vJjES+dDMKa29DAjPtWCXG/pyZ00ufpFZgkGEuAHH5uqUcr1JQg==", "cpu": [ "x64" ], @@ -1593,9 +1649,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.59.tgz", - "integrity": "sha512-z5ZJxizRvRoSAaevRIi3YjQh74OFWEIhonSDWNdqDL7RbjEivcatYcG7OikH6s+rtPhOcwNm3PbGV2Prcgh/gg==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.62.tgz", + "integrity": "sha512-zv14vlF2VRrxS061XkfzGjCYnOrEo5glKJjLK5PwUKysIoVrx/L8nAbFxjkX5cObdlyoqo+ekelyBPAO+4bS0w==", "cpu": [ "arm64" ], @@ -1609,9 +1665,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.59.tgz", - "integrity": "sha512-vxpsn+hrKAhi5YusQfB/JXUJJVX40rIRE/L49ilBEqdbH8Khkoego6AD+2vWqTdJcUHo1WiAIAEZ0rTsjyorLQ==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.62.tgz", + "integrity": "sha512-8MC/PZQSsOP2iA/81tAfNRqMWyEqTS/8zKUI67vPuLvpx6NAjRn3E9qBv7iFqH79iqZNzqSMo3awnLrKZyFbcw==", "cpu": [ "ia32" ], @@ -1625,9 +1681,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.59", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.59.tgz", - "integrity": "sha512-Ris/cJbURylcLwqz4RZUUBCEGsuaIHOJsvf69W5pGKHKBryVoOTNhBKpo3Km2hoAi5qFQ/ou0trAT4hBsVPZvQ==", + "version": "1.3.62", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.62.tgz", + "integrity": "sha512-GJSmUJ95HKHZXAxiuPUmrcm/S3ivQvEzXhOZaIqYBIwUsm02vFZkClsV7eIKzWjso1t0+I/8MjrnUNaSWqh1rQ==", "cpu": [ "x64" ], @@ -1640,6 +1696,22 @@ "node": ">=10" } }, + "node_modules/@swc/jest": { + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.26.tgz", + "integrity": "sha512-7lAi7q7ShTO3E5Gt1Xqf3pIhRbERxR1DUxvtVa9WKzIB+HGQ7wZP5sYx86zqnaEoKKGhmOoZ7gyW0IRu8Br5+A==", + "dev": true, + "dependencies": { + "@jest/create-cache-key-function": "^27.4.2", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -2033,6 +2105,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==", + "dev": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -2344,6 +2422,7 @@ }, "node_modules/callsites": { "version": "3.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2521,6 +2600,7 @@ }, "node_modules/debug": { "version": "4.3.4", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -2964,14 +3044,6 @@ "node": ">=4.0" } }, - "node_modules/esm": { - "version": "3.2.25", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/espree": { "version": "9.4.1", "dev": true, @@ -3571,6 +3643,15 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ip-num": { "version": "1.5.0", "license": "MIT" @@ -3718,16 +3799,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-observable": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "dev": true, @@ -4731,9 +4802,10 @@ } }, "node_modules/marked": { - "version": "4.2.2", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, - "license": "MIT", "bin": { "marked": "bin/marked.js" }, @@ -4806,6 +4878,7 @@ }, "node_modules/ms": { "version": "2.1.2", + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -4906,10 +4979,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/observable-fns": { - "version": "0.6.1", - "license": "MIT" - }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -5272,6 +5341,18 @@ "dev": true, "license": "MIT" }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "dev": true, @@ -5477,14 +5558,49 @@ "node": ">=8" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/shiki": { - "version": "0.10.1", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz", + "integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==", "dev": true, - "license": "MIT", "dependencies": { - "jsonc-parser": "^3.0.0", - "vscode-oniguruma": "^1.6.1", - "vscode-textmate": "5.2.0" + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/side-channel": { @@ -5766,30 +5882,6 @@ "dev": true, "license": "MIT" }, - "node_modules/threads": { - "version": "1.7.0", - "license": "MIT", - "dependencies": { - "callsites": "^3.1.0", - "debug": "^4.2.0", - "is-observable": "^2.1.0", - "observable-fns": "^0.6.1" - }, - "funding": { - "url": "https://github.com/andywer/threads.js?sponsor=1" - }, - "optionalDependencies": { - "tiny-worker": ">= 2" - } - }, - "node_modules/tiny-worker": { - "version": "2.3.0", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "esm": "^3.2.25" - } - }, "node_modules/tmpl": { "version": "1.0.5", "dev": true, @@ -5815,8 +5907,9 @@ } }, "node_modules/ts-custom-error": { - "version": "3.3.1", - "license": "MIT", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.2.tgz", + "integrity": "sha512-u0YCNf2lf6T/vHm+POKZK1yFKWpSpJitcUN3HxqyEcFuNnHIDbyuIQC7QDy/PsBX3giFyk9rt6BFqBAh2lsDZQ==", "engines": { "node": ">=14.0.0" } @@ -6005,67 +6098,55 @@ } }, "node_modules/typedoc": { - "version": "0.22.18", + "version": "0.23.28", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", + "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "glob": "^8.0.3", "lunr": "^2.3.9", - "marked": "^4.0.16", - "minimatch": "^5.1.0", - "shiki": "^0.10.1" + "marked": "^4.2.12", + "minimatch": "^7.1.3", + "shiki": "^0.14.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 12.10.0" + "node": ">= 14.14" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x || 4.7.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/typedoc/node_modules/glob": { - "version": "8.0.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/typedoc/node_modules/minimatch": { - "version": "5.1.0", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/typescript": { - "version": "4.7.4", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6156,14 +6237,16 @@ } }, "node_modules/vscode-oniguruma": { - "version": "1.6.2", - "dev": true, - "license": "MIT" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true }, "node_modules/vscode-textmate": { - "version": "5.2.0", - "dev": true, - "license": "MIT" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true }, "node_modules/walker": { "version": "1.0.8", diff --git a/package.json b/package.json index 785274b6..bce1496c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "prepare": "tsc -p ./tsconfig.build.json", "prebuild": "node ./scripts/prebuild.js", - "build": "rimraf ./dist && tsc -p ./tsconfig.build.json", + "build": "shx rm -rf ./dist && tsc -p ./tsconfig.build.json", "version": "node ./scripts/version.js", "prepublishOnly": "node ./scripts/prepublishOnly.js", "ts-node": "ts-node", @@ -27,16 +27,18 @@ "lint": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}'", "lintfix": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}' --fix", "lint-shell": "find ./src ./tests ./scripts -type f -regextype posix-extended -regex '.*\\.(sh)' -exec shellcheck {} +", - "docs": "rimraf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", + "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", "bench": "rimraf ./benches/results && ts-node ./benches" }, "dependencies": { - "@matrixai/async-init": "^1.8.1", - "@matrixai/async-locks": "^3.1.1", - "@matrixai/errors": "^1.1.2", + "@matrixai/async-cancellable": "^1.1.0", + "@matrixai/contexts": "^1.0.0", + "@matrixai/async-init": "^1.8.3", + "@matrixai/async-locks": "^3.2.0", + "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^3.1.0", "@matrixai/resources": "^1.1.3", - "@matrixai/workers": "^1.3.5", + "@matrixai/timer": "^1.1.0", "ip-num": "^1.5.0" }, "optionalDependencies": { @@ -53,7 +55,8 @@ "@peculiar/asn1-x509": "^2.3.0", "@peculiar/webcrypto": "^1.4.0", "@peculiar/x509": "^1.8.3", - "@swc/core": "^1.2.215", + "@swc/core": "^1.3.62", + "@swc/jest": "^0.2.26", "@types/jest": "^28.1.3", "@types/node": "^18.15.0", "@typescript-eslint/eslint-plugin": "^5.36.2", @@ -68,14 +71,14 @@ "jest-extended": "^3.0.1", "jest-junit": "^14.0.0", "prettier": "^2.6.2", - "rimraf": "^3.0.2", "semver": "^7.3.7", + "shx": "^0.3.4", "sodium-native": "^3.4.1", "systeminformation": "^5.18.5", "ts-jest": "^28.0.5", "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", - "typedoc": "^0.22.15", - "typescript": "^4.5.2" + "typedoc": "^0.23.21", + "typescript": "^4.9.3" } } diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 194e96db..f5416ddd 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -1,11 +1,14 @@ +import type { PromiseCancellable } from '@matrixai/async-cancellable'; +import type { ContextTimed } from '@matrixai/contexts' import type { Crypto, Host, Hostname, Port } from './types'; import type { Config } from './native/types'; -import type { QUICConfig } from './config'; import type QUICConnectionMap from './QUICConnectionMap'; -import type { StreamCodeToReason, StreamReasonToCode } from './types'; +import type { QUICConfig, StreamCodeToReason, StreamReasonToCode } from './types'; import Logger from '@matrixai/logger'; import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy'; import { destroyed, running } from '@matrixai/async-init'; +import { timedCancellable, context } from '@matrixai/contexts/dist/decorators'; + import { quiche } from './native'; import * as utils from './utils'; import * as errors from './errors'; @@ -19,9 +22,19 @@ import QUICConnectionId from './QUICConnectionId'; * You must provide a error handler `addEventListener('error')`. * Otherwise errors will just be ignored. * + * Use the same event names. + * However it needs to bubble up. + * And the right target needs to be used. + * * Events: - * - error - (could be a QUICSocketErrorEvent OR QUICClientErrorEvent) - * - destroy + * - clientError encapsulates: + * - socketError + * - connectionError + * - clientDestroy + * - socketStop + * - connectionStream + * - connectionStop + * - streamDestroy */ interface QUICClient extends CreateDestroy {} @CreateDestroy() @@ -29,86 +42,110 @@ class QUICClient extends EventTarget { public readonly isSocketShared: boolean; protected socket: QUICSocket; protected logger: Logger; - protected crypto: { - key: ArrayBuffer; - ops: Crypto; - }; protected config: Config; protected _connection: QUICConnection; protected connectionMap: QUICConnectionMap; /** * Creates a QUIC Client - * @param options - * @param options.host - target host, if wildcard, it is resolved to its localhost `0.0.0.0` becomes `127.0.0.1` and `::` becomes `::1` - * @param options.port - defaults to 0 - * @param options.localHost - * @param options.localPort + * + * @param opts + * @param opts.host - peer host where `0.0.0.0` becomes `127.0.0.1` and `::` becomes `::1` + * @param opts.port + * @param opts.localHost - defaults to `::` (dualstack) + * @param opts.localPort - defaults 0 + * @param opts.crypto - client only needs the ability to generate random bytes + * @param opts.config - optional config + * @param opts.socket - optional QUICSocket to use + * @param opts.resolveHostname - optional hostname resolver + * @param opts.reasonToCode - optional reason to code map + * @param opts.codeToReason - optional code to reason map + * @param opts.logger - optional logger */ - public static async createQUICClient({ - host, - port, - localHost = '::' as Host, - localPort = 0 as Port, - crypto, - socket, - resolveHostname = utils.resolveHostname, - reasonToCode, - codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, - keepaliveIntervalTime, - logger = new Logger(`${this.name}`), - config = {}, - }: { - host: Host | Hostname; - port: Port; - localHost?: Host | Hostname; - localPort?: Port; - crypto: { - key: ArrayBuffer; - ops: Crypto; - }; - socket?: QUICSocket; - resolveHostname?: (hostname: Hostname) => Host | PromiseLike; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - maxReadableStreamBytes?: number; - maxWritableStreamBytes?: number; - keepaliveIntervalTime?: number; - logger?: Logger; - config?: Partial; - }) { + public static createQUICClient( + opts: { + host: Host | Hostname; + port: Port; + localHost?: Host | Hostname; + localPort?: Port; + crypto: { + ops: { + randomBytes(data: ArrayBuffer): Promise; + }; + }; + config?: Partial; + socket?: QUICSocket; + resolveHostname?: (hostname: Hostname) => Host | PromiseLike; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + logger?: Logger; + }, + ctx?: Partial, + ): PromiseCancellable; + @timedCancellable(true, Infinity, errors.ErrorQUICClientCreateTimeOut) + public static async createQUICClient( + { + host, + port, + localHost = '::' as Host, + localPort = 0 as Port, + crypto, + config = {}, + socket, + resolveHostname = utils.resolveHostname, + reasonToCode, + codeToReason, + logger = new Logger(`${this.name}`), + }: { + host: Host | Hostname; + port: Port; + localHost?: Host | Hostname; + localPort?: Port; + crypto: { + ops: { + randomBytes(data: ArrayBuffer): Promise; + }; + }; + config?: Partial; + socket?: QUICSocket; + resolveHostname?: (hostname: Hostname) => Host | PromiseLike; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + logger?: Logger; + }, + @context ctx: ContextTimed + ): Promise { + let address = utils.buildAddress(host, port); + logger.info(`Create ${this.name} to ${address}`); const quicConfig = { ...clientDefault, ...config, }; + // SCID for the client is randomly generated + // DCID is also randomly generated, but by the quiche library const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); await crypto.ops.randomBytes(scidBuffer); const scid = new QUICConnectionId(scidBuffer); - let address = utils.buildAddress(host, port); - logger.info(`Create ${this.name} to ${address}`); let [host_] = await utils.resolveHost(host, resolveHostname); // If the target host is in fact an zero IP, it cannot be used // as a target host, so we need to resolve it to a non-zero IP // in this case, 0.0.0.0 is resolved to 127.0.0.1 and :: and ::0 is // resolved to ::1 host_ = utils.resolvesZeroIP(host_); - const { p: errorP, rejectP: rejectErrorP } = utils.promise(); + // This error promise is only used during `connection.start()`. + const { + p: socketErrorP, + rejectP: rejectSocketErrorP + } = utils.promise(); const handleQUICSocketError = (e: events.QUICSocketErrorEvent) => { - rejectErrorP(e.detail); - }; - const handleConnectionError = (e: events.QUICConnectionErrorEvent) => { - rejectErrorP(e.detail); + rejectSocketErrorP(e.detail); }; let isSocketShared: boolean; if (socket == null) { socket = new QUICSocket({ - crypto, resolveHostname, logger: logger.getChild(QUICSocket.name), }); - socket.addEventListener('error', handleQUICSocketError, { once: true }); isSocketShared = false; await socket.start({ host: localHost, @@ -120,6 +157,11 @@ class QUICClient extends EventTarget { } isSocketShared = true; } + socket.addEventListener( + 'socketError', + handleQUICSocketError, + { once: true } + ); // Check that the target `host` is compatible with the bound socket host if ( socket.type === 'ipv4' && @@ -149,7 +191,8 @@ class QUICClient extends EventTarget { `Cannot connect to ${host_} an IPv4 mapped IPv6 QUICClient`, ); } - const connection = await QUICConnection.connectQUICConnection({ + const connection = new QUICConnection({ + type: 'client', scid, socket, remoteInfo: { @@ -159,53 +202,33 @@ class QUICClient extends EventTarget { config: quicConfig, reasonToCode, codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, logger: logger.getChild( `${QUICConnection.name} ${scid.toString().slice(32)}`, ), }); - connection.addEventListener('error', handleConnectionError, { once: true }); - logger.debug('CLIENT TRIGGER SEND'); - // This will not raise an error - await connection.send(); - // This will wait to be established, while also rejecting on error + const abortController = new AbortController(); + ctx.signal.addEventListener('abort', (r) => { + abortController.abort(r); + }); try { - await Promise.race([connection.establishedP, errorP]); + await Promise.race([ + await connection.start( + { ...ctx, signal: abortController.signal } + ), + socketErrorP, + ]); } catch (e) { - logger.error(e.toString()); - // Console.error(e); - logger.debug(`Is shared?: ${isSocketShared}`); - // Waiting for connection to destroy - if (connection[destroyed] === false) { - const destroyedProm = utils.promise(); - connection.addEventListener( - 'destroy', - () => { - destroyedProm.resolveP(); - }, - { - once: true, - }, - ); - await destroyedProm.p; - } + // In case the `connection.start` is on-going, we need to abort it + abortController.abort(e); if (!isSocketShared) { - // Stop our own socket + // Stop is idempotent await socket.stop(); } throw e; + } finally { + socket.removeEventListener('socketError', handleQUICSocketError); } - - // Remove the temporary socket error handler - socket.removeEventListener('error', handleQUICSocketError); - // Remove the temporary connection error handler - connection.removeEventListener('error', handleConnectionError); - // Setting up keep alive - connection.setKeepAlive(keepaliveIntervalTime); - // Now we create the client const client = new this({ - crypto, socket, connection, isSocketShared, @@ -217,46 +240,104 @@ class QUICClient extends EventTarget { } /** - * Handle QUIC socket errors - * This is only used if the socket is not shared - * If the socket is shared, then it is expected that the user - * would listen on error events on the socket itself - * Otherwise this will propagate such errors to the server + * This must not throw any exceptions. */ - protected handleQUICSocketError = (e: events.QUICSocketErrorEvent) => { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: e, - }), - ); + protected handleQUICSocketEvents = async (e: events.QUICSocketEvent) => { + if (e instanceof events.QUICSocketErrorEvent) { + // QUIC socket errors are re-emitted but a destroy takes place + this.dispatchEvent( + new events.QUICClientErrorEvent({ + detail: new errors.ErrorQUICClient( + 'Socket error', + { + cause: e.detail, + } + ) + }), + ); + try { + // Force destroy means don't destroy gracefully + await this.destroy({ + force: true + }); + } catch (e) { + this.dispatchEvent( + new events.QUICClientErrorEvent({ + detail: e.detail, + }), + ); + } + } else if (e instanceof events.QUICSocketStopEvent) { + // If a QUIC socket stopped, we immediately destroy + // However, the stop will have its own constraints + try { + // Force destroy means don't destroy gracefully + await this.destroy({ + force: true + }); + } catch (e) { + this.dispatchEvent( + new events.QUICClientErrorEvent({ + detail: e.detail, + }), + ); + } + } else { + this.dispatchEvent(e); + } }; /** - * Handles QUIC connection errors - * This is always used because QUICClient is - * one to one with QUICConnection + * This must not throw any exceptions. */ - protected handleQUICConnectionError = ( - e: events.QUICConnectionErrorEvent, - ) => { - this.dispatchEvent( - new events.QUICClientErrorEvent({ - detail: e, - }), - ); + protected handleQUICConnectionEvents = async (e: events.QUICConnectionEvent) => { + if (e instanceof events.QUICConnectionErrorEvent) { + this.dispatchEvent( + new events.QUICClientErrorEvent({ + detail: new errors.ErrorQUICClient( + 'Connection error', + { + cause: e.detail, + } + ) + }), + ); + try { + // Force destroy means don't destroy gracefully + await this.destroy({ + force: true + }); + } catch (e) { + this.dispatchEvent( + new events.QUICClientErrorEvent({ + detail: e.detail, + }), + ); + } + } else if (e instanceof events.QUICConnectionStopEvent) { + try { + // Force destroy means don't destroy gracefully + await this.destroy({ + force: true + }); + } catch (e) { + this.dispatchEvent( + new events.QUICClientErrorEvent({ + detail: e.detail, + }), + ); + } + } else { + this.dispatchEvent(e); + } }; public constructor({ - crypto, socket, isSocketShared, connection, logger, }: { - crypto: { - key: ArrayBuffer; - ops: Crypto; - }; socket: QUICSocket; isSocketShared: boolean; connection: QUICConnection; @@ -264,15 +345,35 @@ class QUICClient extends EventTarget { }) { super(); this.logger = logger; - this.crypto = crypto; this.socket = socket; this.isSocketShared = isSocketShared; - // Registers itself to the socket - if (!isSocketShared) { - this.socket.addEventListener('error', this.handleQUICSocketError); - } this._connection = connection; - this._connection.addEventListener('error', this.handleQUICConnectionError); + // Listen on all socket events + socket.addEventListener( + 'socketError', + this.handleQUICSocketEvents + ); + socket.addEventListener( + 'socketStop', + this.handleQUICSocketEvents + ); + // Listen on all connection events + connection.addEventListener( + 'connectionStream', + this.handleQUICConnectionEvents + ); + connection.addEventListener( + 'connectionStop', + this.handleQUICConnectionEvents + ); + connection.addEventListener( + 'connectionError', + this.handleQUICConnectionEvents + ); + connection.addEventListener( + 'streamDestroy', + this.handleQUICConnectionEvents + ); } @ready(new errors.ErrorQUICClientDestroyed()) @@ -287,12 +388,18 @@ class QUICClient extends EventTarget { @ready(new errors.ErrorQUICClientDestroyed()) public get connection() { - // This is supposed to return a specialised INTERFACE - // so we aren't just returning QUICConnection - // the difference between internal interface and external interface return this._connection; } + /** + * Force destroy means that we don't destroy gracefully. + * This should only occur when an error occurs from the socket + * or from the connection. If the socket is stopped or the connection + * is stopped, then we also force destroy. + * Suppose the socket failed, and we attempt to stop the connection. + * The connection may attempt to stop gracefully. That would result in + * an exception because the socket send method no longer works. + */ public async destroy({ force = false, }: { @@ -300,20 +407,39 @@ class QUICClient extends EventTarget { } = {}) { const address = utils.buildAddress(this.socket.host, this.socket.port); this.logger.info(`Destroy ${this.constructor.name} on ${address}`); - - // We may want to allow one to specialise this - await this._connection.destroy({ force }); + // Listen on all socket events + this.socket.removeEventListener( + 'socketError', + this.handleQUICSocketEvents + ); + this.socket.removeEventListener( + 'socketStop', + this.handleQUICSocketEvents + ); + // Listen on all connection events + this.connection.removeEventListener( + 'connectionStream', + this.handleQUICConnectionEvents + ); + this.connection.removeEventListener( + 'connectionStop', + this.handleQUICConnectionEvents + ); + this.connection.removeEventListener( + 'connectionError', + this.handleQUICConnectionEvents + ); + this.connection.removeEventListener( + 'streamDestroy', + this.handleQUICConnectionEvents + ); + await this._connection.stop({ force }); if (!this.isSocketShared) { - await this.socket.stop(); - this.socket.removeEventListener('error', this.handleQUICSocketError); + await this.socket.stop({ force }); } this.dispatchEvent(new events.QUICClientDestroyEvent()); this.logger.info(`Destroyed ${this.constructor.name} on ${address}`); } - - // Unlike the server - // upon a connection failing/destroying - // it should result in the CLIENT also being destroyed } export default QUICClient; diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 526ca967..6d05926f 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -1,20 +1,17 @@ +import type { PromiseCancellable } from '@matrixai/async-cancellable'; +import type { ContextTimed } from '@matrixai/contexts'; import type QUICSocket from './QUICSocket'; import type QUICConnectionMap from './QUICConnectionMap'; import type QUICConnectionId from './QUICConnectionId'; -// This is specialized type -import type { QUICConfig } from './config'; import type { Host, Port, RemoteInfo, StreamId } from './types'; import type { Connection, ConnectionErrorCode, SendInfo } from './native/types'; import type { StreamCodeToReason, StreamReasonToCode } from './types'; -import type { ConnectionMetadata } from './types'; -import { - CreateDestroy, - ready, - status, -} from '@matrixai/async-init/dist/CreateDestroy'; +import type { QUICConfig, ConnectionMetadata } from './types'; +import { StartStop, ready, status, running } from '@matrixai/async-init/dist/StartStop'; import Logger from '@matrixai/logger'; -import { Lock } from '@matrixai/async-locks'; +import { Lock, LockBox } from '@matrixai/async-locks'; import { destroyed } from '@matrixai/async-init'; +import { Timer } from '@matrixai/timer'; import { buildQuicheConfig } from './config'; import QUICStream from './QUICStream'; import { quiche } from './native'; @@ -22,6 +19,7 @@ import * as events from './events'; import * as utils from './utils'; import * as errors from './errors'; import { promise } from './utils'; +import { context, timedCancellable } from '@matrixai/contexts/dist/decorators'; /** * Think of this as equivalent to `net.Socket`. @@ -29,41 +27,72 @@ import { promise } from './utils'; * Not to the server. * * Events (events are executed post-facto): - * - stream - when new stream is created - * - destroy - when destruction is done - * - error - when an error is emitted + * - connectionStream + * - connectionStop + * - connectionError - can occur due to a timeout too + * - streamDestroy */ -interface QUICConnection extends CreateDestroy {} -@CreateDestroy() +interface QUICConnection extends StartStop {} +@StartStop() class QUICConnection extends EventTarget { - public readonly connectionId: QUICConnectionId; + /** + * This determines when it is a client or server connection. + */ public readonly type: 'client' | 'server'; - public conn: Connection; - public connectionMap: QUICConnectionMap; - public streamMap: Map = new Map(); - protected reasonToCode: StreamReasonToCode; - protected codeToReason: StreamCodeToReason; - protected maxReadableStreamBytes: number | undefined; - protected maxWritableStreamBytes: number | undefined; - - // This basically allows one to await this promise - // once resolved, always resolved... - // note that this may be rejected... at the beginning - // if the connection setup fails (not sure how this can work yet) - public readonly establishedP: Promise; - protected resolveEstablishedP: () => void; - protected rejectEstablishedP: (reason?: any) => void; - public readonly handshakeP: Promise; - protected resolveHandshakeP: () => void; + /** + * This is the source connection ID. + */ + public readonly connectionId: QUICConnectionId; + + /** + * Internal native connection object. + * @internal + */ + public readonly conn: Connection; + + /** + * Internal conn state transition lock. + * This is used to serialize state transitions to `conn`. + * This is also used by `QUICSocket`. + * @internal + */ + public readonly connLock: Lock = new Lock(); + + /** + * Internal stream map. + * This is also used by `QUICStream`. + * @internal + */ + public readonly streamMap: Map = new Map(); + /** + * Logger. + */ protected logger: Logger; + + /** + * Underlying socket. + */ protected socket: QUICSocket; - protected timer?: ReturnType; - protected keepAliveInterval?: ReturnType; - public readonly closedP: Promise; - protected resolveCloseP?: () => void; + protected config: QUICConfig; + + /** + * Converts reason to code. + * Used during `QUICStream` creation. + */ + protected reasonToCode: StreamReasonToCode; + + /** + * Converts code to reason. + * Used during `QUICStream` creation. + */ + protected codeToReason: StreamCodeToReason; + + /** + * Stream ID increment lock. + */ protected streamIdLock: Lock = new Lock(); /** @@ -78,210 +107,216 @@ class QUICConnection extends EventTarget { */ protected streamIdServerBidi: StreamId = 0b01 as StreamId; - // /** - // * Client initiated unidirectional stream starts at 2. - // * Increment by 4 to get the next ID. - // */ - // protected streamIdClientUni: StreamId = 0b10 as StreamId; + /** + * Client initiated unidirectional stream starts at 2. + * Increment by 4 to get the next ID. + */ + protected streamIdClientUni: StreamId = 0b10 as StreamId; - // /** - // * Server initiated unidirectional stream starts at 3. - // * Increment by 4 to get the next ID. - // */ - // protected streamIdServerUni: StreamId = 0b11 as StreamId; + /** + * Server initiated unidirectional stream starts at 3. + * Increment by 4 to get the next ID. + */ + protected streamIdServerUni: StreamId = 0b11 as StreamId; + + /** + * Internal conn timer. This is used to tick the state transitions on the + * conn. + */ + protected connTimeOutTimer?: Timer; + + /** + * Keep alive timer. + * If the max idle time is set to >0, the connection can time out on idleness. + * Idleness is where there is no response from the other side. This can happen + * from the beginning to the establishment of the connection and while the + * connection is established. Normally there is nothing that will keep the + * connection alive if there is no activity. This keep alive mechanism will + * trigger ping frames to ensure that there is connection activity. + * If the max idle time is set to 0, the connection never times out on idleness. + * However this keep alive mechanism will continue to work in case you need + * activity on the connection for some reason. + * Note that the timer used for the `ContextTimed` in `QUICClient.createQUICClient` + * is independent of the max idle time. This keep alive mechanism will only + * start working after secure establishment. + */ + protected keepAliveIntervalTimer?: Timer; - // These can change on every `recv` call + /** + * This can change on every `recv` call + */ protected _remoteHost: Host; + + /** + * This can change on every `recv` call + */ protected _remotePort: Port; /** - * Create QUICConnection by connecting to a server + * Bubble up all QUIC stream events. + */ + protected handleQUICStreamEvents = (e: events.QUICStreamEvent) => { + this.dispatchEvent(e); + }; + + /** + * Connection establishment. + * This can resolve or reject. + * Rejections cascade down to `secureEstablishedP` and `closedP`. + */ + protected establishedP: Promise; + + /** + * Connection has been verified and secured. + * This can only happen after `establishedP`. + * On the server side, being established means it is also secure established. + * On the client side, after being established, the client must wait for the + * first short frame before it is also secure established. + * This can resolve or reject. + * Rejections cascade down to `closedP`. + */ + protected secureEstablishedP: Promise; + + /** + * Connection closed promise. + * This can resolve or reject. */ - public static async connectQUICConnection({ + protected closedP: Promise; + + + protected wasEstablished: boolean = false; + + protected resolveEstablishedP: () => void; + protected rejectEstablishedP: (reason?: any) => void; + protected resolveSecureEstablishedP: () => void; + protected rejectSecureEstablishedP: (reason?: any) => void; + protected resolveClosedP: () => void; + protected rejectClosedP: (reason?: any) => void; + + protected lastErrorMessage?: string; + + public constructor({ + type, scid, - socket, + dcid, remoteInfo, config, + socket, reasonToCode = () => 0, - codeToReason = (type, code) => - new Error(`${type.toString()} ${code.toString()}`), - maxReadableStreamBytes, - maxWritableStreamBytes, - logger = new Logger(`${this.name} ${scid}`), + codeToReason = (type, code) => new Error(`${type} ${code}`), + logger, }: { + type: 'client'; scid: QUICConnectionId; - socket: QUICSocket; + dcid?: undefined; remoteInfo: RemoteInfo; config: QUICConfig; + socket: QUICSocket; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - maxReadableStreamBytes?: number; - maxWritableStreamBytes?: number; logger?: Logger; - }) { - logger.info(`Connect ${this.name}`); - const quicheConfig = buildQuicheConfig(config); - const conn = quiche.Connection.connect( - null, - scid, - { - host: socket.host, - port: socket.port, - }, - { - host: remoteInfo.host, - port: remoteInfo.port, - }, - quicheConfig, - ); - // This will output to the log keys file path - if (config.logKeys != null) { - conn.setKeylog(config.logKeys); - } - const connection = new this({ - type: 'client', - conn, - connectionId: scid, - socket, - remoteInfo, - reasonToCode, - codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, - logger, - }); - socket.connectionMap.set(connection.connectionId, connection); - logger.info(`Connected ${this.name}`); - return connection; - } - - /** - * Create QUICConnection by accepting a client - */ - public static async acceptQUICConnection({ - scid, - dcid, - socket, - remoteInfo, - config, - reasonToCode = () => 0, - codeToReason = (type, code) => - new Error(`${type.toString()} ${code.toString()}`), - maxReadableStreamBytes, - maxWritableStreamBytes, - logger = new Logger(`${this.name} ${scid}`), - }: { + } | { + type: 'server'; scid: QUICConnectionId; dcid: QUICConnectionId; - socket: QUICSocket; remoteInfo: RemoteInfo; config: QUICConfig; + socket: QUICSocket; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - maxReadableStreamBytes?: number; - maxWritableStreamBytes?: number; logger?: Logger; - }): Promise { - logger.info(`Accept ${this.name}`); + }) { + super(); + this.logger = logger ?? new Logger(`${this.constructor.name} ${scid}`); const quicheConfig = buildQuicheConfig(config); - const conn = quiche.Connection.accept( - scid, - dcid, - { - host: socket.host, - port: socket.port, - }, - { - host: remoteInfo.host, - port: remoteInfo.port, - }, - quicheConfig, - ); + let conn: Connection; + if (type === 'client') { + // This message will be connected to the `this.start` + this.logger.info(`Connect ${this.constructor.name}`); + conn = quiche.Connection.connect( + null, + scid, + { + host: socket.host, + port: socket.port, + }, + { + host: remoteInfo.host, + port: remoteInfo.port, + }, + quicheConfig, + ); + } else if (type === 'server') { + // This message will be connected to `this.start` + this.logger.info(`Accept ${this.constructor.name}`); + conn = quiche.Connection.accept( + scid, + dcid, + { + host: socket.host, + port: socket.port, + }, + { + host: remoteInfo.host, + port: remoteInfo.port, + }, + quicheConfig, + ); + } // This will output to the log keys file path if (config.logKeys != null) { - conn.setKeylog(config.logKeys); + conn!.setKeylog(config.logKeys); } - const connection = new this({ - type: 'server', - conn, - connectionId: scid, - socket, - remoteInfo, - reasonToCode, - codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, - logger, - }); - socket.connectionMap.set(connection.connectionId, connection); - logger.info(`Accepted ${this.name}`); - return connection; - } - - public constructor({ - type, - conn, - connectionId, - socket, - remoteInfo, - reasonToCode, - codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, - logger, - }: { - type: 'client' | 'server'; - conn: Connection; - connectionId: QUICConnectionId; - socket: QUICSocket; - remoteInfo: RemoteInfo; - reasonToCode: StreamReasonToCode; - codeToReason: StreamCodeToReason; - maxReadableStreamBytes: number | undefined; - maxWritableStreamBytes: number | undefined; - logger: Logger; - }) { - super(); - this.logger = logger; this.type = type; - this.conn = conn; - this.connectionId = connectionId; - this.connectionMap = socket.connectionMap; + this.conn = conn!; + this.connectionId = scid; this.socket = socket; - this._remoteHost = remoteInfo.host; - this._remotePort = remoteInfo.port; + this.config = config; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; - this.maxReadableStreamBytes = maxReadableStreamBytes; - this.maxWritableStreamBytes = maxWritableStreamBytes; - // Sets the timeout on the first - this.checkTimeout(); - - // Note that you must be able to reject too - // otherwise one might await for establishment forever - // the server side has to code up their own bootstrap - // but the client side just uses the quiche library + this._remoteHost = remoteInfo.host; + this._remotePort = remoteInfo.port; const { p: establishedP, resolveP: resolveEstablishedP, rejectP: rejectEstablishedP, } = utils.promise(); this.establishedP = establishedP; - this.resolveEstablishedP = resolveEstablishedP; + this.resolveEstablishedP = () => { + + // Upon the first time you call this, it is now true + // But prior to this + + this.wasEstablished = true; + resolveEstablishedP(); + }; this.rejectEstablishedP = rejectEstablishedP; - const { p: closedP, resolveP: resolveClosedP } = utils.promise(); - this.resolveCloseP = resolveClosedP; - this.closedP = closedP; - const { p: handshakeP, resolveP: resolveHandshakeP } = utils.promise(); - this.handshakeP = handshakeP; - this.resolveHandshakeP = resolveHandshakeP; - } - // Immediately call this after construction - // if you want to pass the key log to something - // note that you must close the file descriptor afterward - public setKeylog(path) { - this.conn.setKeylog(path); + // We can do something where, once you "resolve" + // Then it is in fact established + // Alternatively, we can just bind into the `establishedP` here + // Does this become a dangling promsie? + // Cause it is `void` here + // I think it's better to use the `resolveEstablishedP` + + + + const { + p: secureEstablishedP, + resolveP: resolveSecureEstablishedP, + rejectP: rejectSecureEstablishedP + } = utils.promise(); + this.secureEstablishedP = secureEstablishedP; + this.resolveSecureEstablishedP = resolveSecureEstablishedP; + this.rejectSecureEstablishedP = rejectSecureEstablishedP; + const { + p: closedP, + resolveP: resolveClosedP, + rejectP: rejectClosedP, + } = utils.promise(); + this.closedP = closedP; + this.resolveClosedP = resolveClosedP; + this.rejectClosedP = rejectClosedP; } public get remoteHost() { @@ -300,95 +335,132 @@ class QUICConnection extends EventTarget { return this.socket.port; } - public get remoteInfo(): ConnectionMetadata { - const derCerts = this.conn.peerCertChain(); - const remoteCertificates = - derCerts != null - ? derCerts.map((der) => utils.certificateDERToPEM(der)) - : null; - return { - remoteCertificates, - localHost: this.localHost, - localPort: this.localPort, - remoteHost: this.remoteHost, - remotePort: this.remotePort, - }; + /** + * This is the same as basically waiting for `secureEstablishedP` + * While this is occurring one can call the `recv` and `send` to make this happen + */ + public start(ctx?: Partial): PromiseCancellable; + @timedCancellable(true, Infinity, errors.ErrorQUICConnectionStartTimeOut) + public async start(@context ctx: ContextTimed): Promise { + this.logger.info(`Start ${this.constructor.name}`); + ctx.signal.throwIfAborted(); + ctx.signal.addEventListener('abort', (r) => { + this.rejectEstablishedP(r); + this.rejectSecureEstablishedP(r); + + // Is this actually true? + // Technically the connection is closed + this.rejectClosedP(r); + }); + // Set the connection up + this.socket.connectionMap.set(this.connectionId, this); + // Waits for the first short packet after establishment + // This ensures that TLS has been established and verified on both sides + await this.secureEstablishedP; + // After this is done + // We need to established the keep alive interval time + if (this.config.keepAliveIntervalTime != null) { + this.startKeepAliveIntervalTimer( + this.config.keepAliveIntervalTime + ); + } + // Do we remove the on abort event listener? + // I forgot... + this.logger.info(`Started ${this.constructor.name}`); } /** - * This provides the ability to destroy with a specific error. This will wait for the connection to fully drain. + * The `applicationError` if the connection close is due to the transport + * layer or due to the application layer. + * If `applicationError` is true, you can use any number as the `errorCode`. + * The other peer must should understand the `errorCode`. + * If `applicationError` is false, you must use `errorCode` from + * `ConnectionErrorCode`. + * The default `applicationError` is true because a normal graceful close + * is an application error. + * The default `errorCode` of 0 means no error or general error. + * This is the same as basically waiting for `closedP`. */ - public async destroy({ - appError = false, - errorCode = quiche.ConnectionErrorCode.NoError, - errorMessage = '', - force = false, - }: { - appError?: boolean; - errorCode?: ConnectionErrorCode; - errorMessage?: string; - force?: boolean; - } = {}) { - this.logger.info(`Destroy ${this.constructor.name}`); - // Clean up keep alive - if (this.keepAliveInterval != null) { - clearTimeout(this.keepAliveInterval); - delete this.keepAliveInterval; - } - // Handle destruction concurrently - const destroyProms: Array> = []; + public async stop( + { + applicationError = true, + errorCode = 0, + errorMessage = '', + force = false + }: { + applicationError?: false; + errorCode?: ConnectionErrorCode; + errorMessage?: string; + force?: boolean; + } | { + applicationError: true; + errorCode?: number; + errorMessage?: string; + force?: boolean; + }= {}, + lock: Lock = this.connLock + ) { + this.logger.info(`Stop ${this.constructor.name}`); + const streamDestroyPs: Array> = []; for (const stream of this.streamMap.values()) { - if (force) { - destroyProms.push(stream.destroy()); - } else { - const destroyProm = promise(); - stream.addEventListener('destroy', () => destroyProm.resolveP(), { - once: true, - }); - destroyProms.push(destroyProm.p); - } + // TODO: ensure that `stream.destroy` understands `force` + // Without it, it should be graceful + // With it, then it should assume the rest of the system could be broken + streamDestroyPs.push(stream.destroy({ force })); } - await Promise.all(destroyProms); + await Promise.all(streamDestroyPs); + // Do we do this afterwards or before? + this.stopKeepAliveIntervalTimer(); try { - // If this is already closed, then `Done` will be thrown - // Otherwise it can send `CONNECTION_CLOSE` frame - // This can be 0x1c close at the QUIC layer or no errors - // Or it can be 0x1d for application close with an error - // Upon receiving a `CONNECTION_CLOSE`, you can send back - // 1 packet containing a `CONNECTION_CLOSE` frame too - // (with `NO_ERROR` code if appropriate) - // It must enter into a draining state, and no other packets can be sent - this.conn.close(appError, errorCode, Buffer.from(errorMessage)); + // We need to lock the connLock + // Note that this has no timeout + // We must have any deadlocks here! + // Plus ctx isn't accepted by the async-locks yet + // If already closed this will error out + // But nothing will happen + await this.connLock.withF(async () => { + // If this is already closed, then `Done` will be thrown + // Otherwise it can send `CONNECTION_CLOSE` frame + // This can be 0x1c close at the QUIC layer or no errors + // Or it can be 0x1d for application close with an error + // Upon receiving a `CONNECTION_CLOSE`, you can send back + // 1 packet containing a `CONNECTION_CLOSE` frame too + // (with `NO_ERROR` code if appropriate) + // It must enter into a draining state, and no other packets can be sent + this.conn.close(applicationError, errorCode, Buffer.from(errorMessage)); + // If we get a `Done` exception we don't bother calling send + // The send only gets sent if the `Done` is not the case + await this.send(); + }); } catch (e) { + // If the connection is already closed, `Done` will be thrown if (e.message !== 'Done') { - this.logger.debug('already closed'); // No other exceptions are expected utils.never(); } } - // Sending if - await this.send(); - // If it is not closed, it could still be draining - this.logger.debug('Waiting for closeP'); + + // Now we await for the closedP await this.closedP; - this.logger.debug('closeP resolved'); - this.connectionMap.delete(this.connectionId); - // Checking if timed out - if (this.conn.isTimedOut()) { - this.logger.error('Connection timed out'); - this.dispatchEvent( - new events.QUICSocketErrorEvent({ - detail: new errors.ErrorQUICConnectionTimeout(), - }), - ); - } - this.dispatchEvent(new events.QUICConnectionDestroyEvent()); - // Clean up timeout if it's still running - if (this.timer != null) { - clearTimeout(this.timer); - delete this.timer; - } - this.logger.info(`Destroyed ${this.constructor.name}`); + + // The reason we only delete afterwards + // Is because we do it before we are opened (or just constructed) + // Techincally it was constructed, and then we added ourselves to it + // But during `start` we are just waiting + this.socket.connectionMap.delete(this.connectionId); + + this.dispatchEvent(new events.QUICConnectionStopEvent()); + this.logger.info(`Stopped ${this.constructor.name}`); + } + + /** + * Gets an array of certificates in PEM format start on the leaf. + */ + @ready(new errors.ErrorQUICConnectionNotRunning()) + public getRemoteCertsChain(): Array { + const certsDER = this.conn.peerCertChain(); + if (certsDER == null) return []; + return certsDER.map(utils.certificateDERToPEM); } /** @@ -397,17 +469,18 @@ class QUICConnection extends EventTarget { * This pushes data to the streams. * When the connection is draining, we can still receive data. * However, no streams are allowed to read or write. + * + * This method must not throw any exceptions. + * Any errors must be emitted as events. + * @internal */ - @ready(new errors.ErrorQUICConnectionDestroyed(), false, ['destroying']) - public async recv(data: Uint8Array, remoteInfo: RemoteInfo) { - this.logger.debug('RECV CALLED'); + public recv(data: Uint8Array, remoteInfo: RemoteInfo) { try { - // The remote info may have changed on each receive - // here we update! - // This still requires testing to see what happens + // The remote information may be changed on each receive + // However to do so would mean connection migration, + // which is is not yet supported this._remoteHost = remoteInfo.host; this._remotePort = remoteInfo.port; - // Used by quiche const recvInfo = { to: { host: this.localHost, @@ -419,46 +492,79 @@ class QUICConnection extends EventTarget { }, }; try { + // This can process concatenated QUIC packets + // This may mutate `data` this.conn.recv(data, recvInfo); - this.logger.info(`RECEIVED ${data.byteLength} of data`); } catch (e) { - this.logger.error(`recv error ${e.message}`); - // Depending on the exception, the `this.conn.recv` - // may have automatically started closing the connection - if (e.message === 'TlsFail') { - const newError = new errors.ErrorQUICConnectionTLSFailure(undefined, { - data: { - localError: this.conn.localError(), - peerError: this.conn.peerError(), - }, - }); - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ detail: newError }), - ); - } else { - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnection(e.message, { - cause: e, - data: { - localError: this.conn.localError(), - peerError: this.conn.peerError(), - }, - }), - }), - ); + if (e.message !== 'TlsFail') { + // No other exceptions are expected + utils.never(); } - return; + // Do note that if we get a TlsFail + // We must proceed without throwing any exceptions + // But we must "save" the error here + // We don't need the save the localError or remoteError + // Cause that will be "saved" + // Only this e.message <- this will be whatever is the last message + this.lastErrorMessage = e.message; } - this.dispatchEvent(new events.QUICConnectionRecvEvent()); - // Here we can resolve our promises! + + // We don't actually "fail" + // the closedP until we proceed + // But note that if there's an error + if (this.conn.isEstablished()) { this.resolveEstablishedP(); + + + if (this.type === 'server') { + // For server connections, if we are established + // we are secure established + this.resolveSecureEstablishedP(); + } else if (this.type === 'client') { + + // We need a hueristic to indicate whether we are securely established + + // If we are already established + // AND IF, we are getting a packet after establishment + // And we didn't result in an error + // Neither draining, nor closed, nor timed out + + // For server connections + // If we are already established, then we are secure established + + // To know if the server is also established + // We need to know the NEXT recv after we are already established + // So we received something, and that allows us to be established + // UPON the next recv + // We need to ensure: + // 1. No errors + // 2. Not draining + // 3. No + // YES the main thing is that there is no errors + // I think that's the KEY + // But we must only switch + // If were "already" established + // That this wasn't the first time we were established + + } } + + // We also need to know whether this is our first short frame + // After we are already established + // This may not be robust + // Cause technically + // What if we were "concatenated" packet + // Then it could be a problem right? + + if (this.conn.isClosed()) { - if (this.resolveCloseP != null) this.resolveCloseP(); + this.resolveClosedP(); return; } + + + if (this.conn.isInEarlyData() || this.conn.isEstablished()) { const readIds: Array = []; for (const streamId of this.conn.readable() as Iterable) { @@ -470,8 +576,8 @@ class QUICConnection extends EventTarget { connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - maxReadableStreamBytes: this.maxReadableStreamBytes, - maxWritableStreamBytes: this.maxWritableStreamBytes, + // maxReadableStreamBytes: this.maxReadableStreamBytes, + // maxWritableStreamBytes: this.maxWritableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), }); this.dispatchEvent( @@ -495,7 +601,7 @@ class QUICConnection extends EventTarget { connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - maxReadableStreamBytes: this.maxReadableStreamBytes, + // maxReadableStreamBytes: this.maxReadableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), }); this.dispatchEvent( @@ -545,73 +651,28 @@ class QUICConnection extends EventTarget { * * We can push the connection into the stream. * The streams have access to the connection object. + * + * This method must not throw any exceptions. + * Any errors must be emitted as events. + * @internal */ - @ready(new errors.ErrorQUICConnectionDestroyed(), false, ['destroying']) - public async send(): Promise { - this.logger.debug('SEND CALLED'); - if (this.conn.isClosed()) { - if (this.resolveCloseP != null) this.resolveCloseP(); - return; - } else if (this.conn.isDraining()) { - return; - } - let numSent = 0; - try { + public async send(lock: Lock = this.connLock): Promise { + + await this.connLock.withF(async () => { const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE); let sendLength: number; let sendInfo: SendInfo; - while (true) { - try { - this.logger.debug('Did a send'); - [sendLength, sendInfo] = this.conn.send(sendBuffer); - } catch (e) { - this.logger.debug(`SEND FAILED WITH ${e.message}`); - if (e.message === 'Done') { - if (this.conn.isClosed()) { - this.logger.debug('SEND CLOSED'); - if (this.resolveCloseP != null) this.resolveCloseP(); - return; - } - this.logger.debug('SEND IS DONE'); - return; - } - this.logger.error('Failed to send, cleaning up'); + try { + // Send until `Done` + while (true) { try { - // If the `this.conn.send` failed, then this close - // may not be able to be sent to the outside - // It's possible a second call to `this.conn.send` will succeed - // Otherwise a timeout will occur, which will eventually destroy - // this connection - - this.conn.close( - false, - quiche.ConnectionErrorCode.InternalError, - Buffer.from('Failed to send data', 'utf-8'), // The message! - ); + [sendLength, sendInfo] = this.conn.send(sendBuffer); } catch (e) { - // Only `Done` is possible, no other errors are possible - if (e.message !== 'Done') { - utils.never(); + if (e.message === 'Done') { + break; } + throw e; } - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnection(e.message, { - cause: e, - data: { - localError: this.conn.localError(), - peerError: this.conn.peerError(), - }, - }), - }), - ); - return; - } - try { - this.logger.debug( - `ATTEMPTING SEND ${sendLength} bytes to ${sendInfo.to.port}:${sendInfo.to.host}`, - ); - await this.socket.send( sendBuffer, 0, @@ -619,45 +680,186 @@ class QUICConnection extends EventTarget { sendInfo.to.port, sendInfo.to.host, ); - this.logger.info(`SENT ${sendLength} of data`); - } catch (e) { - this.logger.error(`send error ${e.message}`); - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ detail: e }), - ); - return; } - this.dispatchEvent(new events.QUICConnectionSendEvent()); - numSent += 1; + } catch (e) { + + // If called `stop` due to an error here + // we MUST not call `this.send` again + // in fact, we do a hard-stop + // There's no need to even have a timeout at all + // Remember this exception COULD be due to `e` + // It could be due to `localError` or `remoteError` + // All of this is possbile + // Generally at least one of them is the reason + + // the error has to be one or the other + + await this.stop({ + error: e + }); + + // We need to finish without any exceptions + return; } - } finally { - if (numSent > 0) this.garbageCollectStreams('send'); - this.logger.debug('SEND FINALLY'); - this.checkTimeout(); - if ( - this[status] !== 'destroying' && - (this.conn.isClosed() || this.conn.isDraining()) - ) { - // Ignore errors and run in background - void this.destroy().catch(() => {}); - } else if ( - this[status] === 'destroying' && - this.conn.isClosed() && - this.resolveCloseP != null - ) { - // If we flushed the draining, then this is what will happen - this.resolveCloseP(); + if (this.conn.isClosed()) { + + // But if it is closed with no error + // Then we just have to proceed! + // Plus if we are called here + + await this.stop({ + error: this.conn.localError() ?? this.conn.remoteError(), + }); + + } else { + // In all other cases, reset the conn timer + this.setConnTimeOutTimer(); + } + }); + } + + protected setConnTimeOutTimer(): void { + const connTimeOutHandler = async () => { + // This can only be called when the timeout has occurred + // This transitions the connection state + this.conn.onTimeout(); + + // At this point... + // we check the conditions on the connection + // This way we can RESOLVE things like + // conn closed or established or other things + // or if we are draining + // if we have timed out... etc + // All state changes need to result in reaction + // Is established is not required + // But we can have a bunch of things that check and react accordingly + // So if it is timed out, it gets closed too + // But if it is is timed out due to idle we raise an error + + if (this.conn.isTimedOut()) { + + // This is just a dispatch on the connection error + // Note that this may cause the client to attempt + // to stop the socket and stuff + // The client should ignore this event error + // Becuase it's actually being handled + // On the other hand... + // If we randomly fail here + // It's correct to properly raise an event + // To bubble up.... + + this.dispatchEvent( + new events.QUICConnectionErrorEvent({ + detail: new errors.ErrorQUICConnectionTimeout() + }) + ); } + + // At the same time, we may in fact be closed too + if (this.conn.isClosed()) { + // We actually finally closed here + // Actually theq uestion is that this could be an error + // The act of closing is an error? + // That's confusing + this.resolveClosedP(); + // If we are not stopping nor are we stopped + // And we are not running, call await this stop + + // We need to trigger this as well by calling stop + // What happens if we are starting too? + if (this[running] && this[status] !== 'stopping') { + // If we are already stopping, stop multiple times is idempotent + // Wait if we call stop multiple times + // Actually we may already be stopping + // But also that if the status is starting + // But also if we are starting + // Resolve the closeP + // is technicaly an error! + + await this.stop(); + } + + // Finish + return; + } + + // Note that a `0` timeout is still a valid timeout + const timeout = this.conn.timeout(); + // If this is `null`, then technically there's nothing to do + if (timeout == null) return; + this.connTimeOutTimer = new Timer({ + delay: timeout, + handler: connTimeOutHandler + }); + }; + // Note that a `0` timeout is still a valid timeout + const timeout = this.conn.timeout(); + // If this is `null` there's nothing to do + if (timeout == null) return; + // If there was an existing timer, we cancel it and set a new one + if (this.connTimeOutTimer != null) { + this.connTimeOutTimer.cancel(); } + this.connTimeOutTimer = new Timer({ + delay: timeout, + handler: connTimeOutHandler + }); + } + + /** + * Starts the keep alive interval timer. + * Make sure to set the interval to be less than then the `maxIdleTime` unless + * if the `maxIdleTime` is `0`. + * If the `maxIdleTime` is `0`, then this is not needed to keep the connection + * open. However it can still be useful to maintain liveness for NAT purposes. + */ + protected startKeepAliveIntervalTimer(ms: number): void { + const keepAliveHandler = async () => { + // Intelligently schedule a PING frame. + // If the connection has already sent ack-eliciting frames + // then this is a noop. + await this.connLock.withF(async () => { + this.conn.sendAckEliciting(); + await this.send(); + }); + this.keepAliveIntervalTimer = new Timer({ + delay: ms, + handler: keepAliveHandler + }); + }; + this.keepAliveIntervalTimer = new Timer({ + delay: ms, + handler: keepAliveHandler + }); } + /** + * Stops the keep alive interval timer + */ + protected stopKeepAliveIntervalTimer(): void { + this.keepAliveIntervalTimer?.cancel(); + } + + + + + + + + + /** * Creates a new stream on the connection. * Only supports bidi streams atm. * This is a serialised call, it must be blocking. */ - @ready(new errors.ErrorQUICConnectionDestroyed()) + @ready(new errors.ErrorQUICConnectionNotRunning()) public async streamNew(streamType: 'bidi' = 'bidi'): Promise { + + // You wouldn't want AsyncMonitor here + // The problem is that we want re-entrant contexts + + // Technically you can do concurrent bidi and uni style streams // but no support for uni streams yet // So we don't bother with it @@ -689,8 +891,8 @@ class QUICConnection extends EventTarget { connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - maxReadableStreamBytes: this.maxReadableStreamBytes, - maxWritableStreamBytes: this.maxWritableStreamBytes, + // maxReadableStreamBytes: this.maxReadableStreamBytes, + // maxWritableStreamBytes: this.maxWritableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId!}`), }); // Ok the stream is opened and working @@ -703,49 +905,57 @@ class QUICConnection extends EventTarget { }); } - /** - * Used to update or disable the keep alive interval. - * Calling this will reset the delay before the next keep alive. - */ - @ready(new errors.ErrorQUICConnectionDestroyed()) - public setKeepAlive(intervalDelay?: number) { - // Clearing timeout prior to update - if (this.keepAliveInterval != null) { - clearTimeout(this.keepAliveInterval); - delete this.keepAliveInterval; - } - // Setting up keep alive interval - if (intervalDelay != null) { - this.keepAliveInterval = setInterval(async () => { - // Trigger an ping frame and send - this.conn.sendAckEliciting(); - await this.send(); - }, intervalDelay); - } - } + // /** + // * Used to update or disable the keep alive interval. + // * Calling this will reset the delay before the next keep alive. + // */ + // @ready(new errors.ErrorQUICConnectionNotRunning()) + // public setKeepAlive(intervalDelay?: number) { + // // Clearing timeout prior to update + // if (this.keepAliveInterval != null) { + // clearTimeout(this.keepAliveInterval); + // delete this.keepAliveInterval; + // } + // // Setting up keep alive interval + // if (intervalDelay != null) { + // this.keepAliveInterval = setInterval(async () => { + // // Trigger an ping frame and send + // this.conn.sendAckEliciting(); + // await this.send(); + // }, intervalDelay); + // } + // } + + // // Timeout handling, these methods handle time keeping for quiche. + // // Quiche will request an amount of time, We then call `onTimeout()` after that time has passed. + // protected deadline: number = 0; + // protected onTimeout = async () => { + // this.logger.warn('ON TIMEOUT CALLED ' + new Date()); + // this.logger.debug('timeout on timeout'); + // // Clearing timeout + // clearTimeout(this.timer); + // delete this.timer; + // this.deadline = Infinity; + // // Doing timeout actions + // // console.time('INTERNAL ON TIMEOUT'); + // this.conn.onTimeout(); + // // console.timeEnd('INTERNAL ON TIMEOUT'); + // this.logger.warn('BEFORE CALLING SEND' + new Date()); + // if (this[destroyed] === false) await this.send(); + // this.logger.warn('AFTER CALLING SEND ' + new Date()); + // if ( + // this[status] !== 'destroying' && + // (this.conn.isClosed() || this.conn.isDraining()) + // ) { + // this.logger.debug('CALLING DESTROY 3'); + // // Destroy in the background, we still need to process packets + // void this.destroy().catch(() => {}); + // } + // this.logger.warn('BEFORE CHECK TIMEOUT' + new Date()); + // this.checkTimeout(); + // this.logger.warn('AFTER CHECK TIMEOUT' + new Date()); + // }; - // Timeout handling, these methods handle time keeping for quiche. - // Quiche will request an amount of time, We then call `onTimeout()` after that time has passed. - protected deadline: number = 0; - protected onTimeout = async () => { - this.logger.debug('timeout on timeout'); - // Clearing timeout - clearTimeout(this.timer); - delete this.timer; - this.deadline = Infinity; - // Doing timeout actions - this.conn.onTimeout(); - if (this[destroyed] === false) await this.send(); - if ( - this[status] !== 'destroying' && - (this.conn.isClosed() || this.conn.isDraining()) - ) { - this.logger.debug('CALLING DESTROY 3'); - // Destroy in the background, we still need to process packets - void this.destroy().catch(() => {}); - } - this.checkTimeout(); - }; /** * Checks the timeout event, should be called whenever the following events happen. * 1. `send()` is called @@ -757,56 +967,65 @@ class QUICConnection extends EventTarget { * 2. Update the timer if `conn.timeout()` is less than current timeout. * 3. clean up timer if `conn.timeout()` is null. */ - protected checkTimeout = () => { - this.logger.debug('timeout checking timeout'); - // During construction, this ends up being null - const time = this.conn.timeout(); - if (time == null) { - // Clear timeout - if (this.timer != null) this.logger.debug('timeout clearing timeout'); - clearTimeout(this.timer); - delete this.timer; - this.deadline = Infinity; - } else { - const newDeadline = Date.now() + time; - if (this.timer != null) { - if (time === 0) { - this.logger.debug('timeout triggering instant timeout'); - // Skip timer and call onTimeout - setImmediate(this.onTimeout); - } else if (newDeadline < this.deadline) { - this.logger.debug(`timeout updating timer with ${time} delay`); - clearTimeout(this.timer); - delete this.timer; - this.deadline = newDeadline; - this.timer = setTimeout(this.onTimeout, time); - } - } else { - if (time === 0) { - this.logger.debug('timeout triggering instant timeout'); - // Skip timer and call onTimeout - setImmediate(this.onTimeout); - return; - } - this.logger.debug(`timeout creating timer with ${time} delay`); - this.deadline = newDeadline; - this.timer = setTimeout(this.onTimeout, time); - } - } - }; + // protected checkTimeout = () => { + // this.logger.debug('timeout checking timeout'); + // // During construction, this ends up being null + // const time = this.conn.timeout(); + // this.logger.error(`THE TIME (${this.times}): ` + time + ' ' + new Date()); + // this.times++; - protected garbageCollectStreams(where: string) { - const nums: Array = []; - // Only check if packets were sent - for (const [streamId, quicStream] of this.streamMap) { - // Stream sending can finish after a packet is sent - nums.push(streamId); - quicStream.read(); - } - if (nums.length > 0) { - this.logger.info(`checking read finally ${where} for ${nums}`); - } - } + // if (time == null) { + // // Clear timeout + // if (this.timer != null) this.logger.debug('timeout clearing timeout'); + // clearTimeout(this.timer); + // delete this.timer; + // this.deadline = Infinity; + // } else { + // const newDeadline = Date.now() + time; + // if (this.timer != null) { + // if (time === 0) { + // this.logger.debug('timeout triggering instant timeout'); + // // Skip timer and call onTimeout + // setImmediate(this.onTimeout); + // } else if (newDeadline < this.deadline) { + // this.logger.debug(`timeout updating timer with ${time} delay`); + // clearTimeout(this.timer); + // delete this.timer; + // this.deadline = newDeadline; + + // this.logger.warn('BEFORE SET TIMEOUT 1: ' + time); + + // this.timer = setTimeout(this.onTimeout, time); + // } + // } else { + // if (time === 0) { + // this.logger.debug('timeout triggering instant timeout'); + // // Skip timer and call onTimeout + // setImmediate(this.onTimeout); + // return; + // } + // this.logger.debug(`timeout creating timer with ${time} delay`); + // this.deadline = newDeadline; + + // this.logger.warn('BEFORE SET TIMEOUT 2: ' + time); + + // this.timer = setTimeout(this.onTimeout, time); + // } + // } + // }; + + // protected garbageCollectStreams(where: string) { + // const nums: Array = []; + // // Only check if packets were sent + // for (const [streamId, quicStream] of this.streamMap) { + // // Stream sending can finish after a packet is sent + // nums.push(streamId); + // quicStream.read(); + // } + // if (nums.length > 0) { + // this.logger.info(`checking read finally ${where} for ${nums}`); + // } + // } } export default QUICConnection; diff --git a/src/QUICServer.ts b/src/QUICServer.ts index 2d79133b..1117bd2f 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -7,10 +7,10 @@ import type { RemoteInfo, StreamCodeToReason, StreamReasonToCode, + QUICConfig, } from './types'; import type { Header } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; -import type { QUICConfig, TlsConfig } from './config'; import type { QUICServerConnectionEvent } from './events'; import Logger from '@matrixai/logger'; import { running } from '@matrixai/async-init'; @@ -30,9 +30,15 @@ import QUICSocket from './QUICSocket'; * Otherwise errors will just be ignored. * * Events: - * - connection - * - error - (could be a QUICSocketErrorEvent OR QUICServerErrorEvent) - * - stop + * - serverStop + * - serverError - (could be a QUICSocketErrorEvent OR QUICServerErrorEvent) + * - serverConnection + * - connectionStream - when new stream is created from a connection + * - connectionError - connection error event + * - connectionDestroy - when connection is destroyed + * - streamDestroy - when stream is destroyed + * - socketError - this also results in a server error + * - socketStop */ interface QUICServer extends StartStop {} @StartStop() @@ -42,58 +48,66 @@ class QUICServer extends EventTarget { protected logger: Logger; protected crypto: { key: ArrayBuffer; - ops: Crypto; + ops: { + sign(key: ArrayBuffer, data: ArrayBuffer): Promise; + verify( + key: ArrayBuffer, + data: ArrayBuffer, + sig: ArrayBuffer, + ): Promise; + }; }; protected config: QUICConfig; protected socket: QUICSocket; protected reasonToCode: StreamReasonToCode | undefined; protected codeToReason: StreamCodeToReason | undefined; - protected maxReadableStreamBytes?: number | undefined; - protected maxWritableStreamBytes?: number | undefined; protected keepaliveIntervalTime?: number | undefined; protected connectionMap: QUICConnectionMap; - /** - * Handle QUIC socket errors - * This is only used if the socket is not shared - * If the socket is shared, then it is expected that the user - * would listen on error events on the socket itself - * Otherwise this will propagate such errors to the server - */ - protected handleQUICSocketError = (e: events.QUICSocketErrorEvent) => { - this.dispatchEvent( - new events.QUICServerErrorEvent({ - detail: e, - }), - ); + protected handleQUICSocketEvents = (e: events.QUICSocketEvent) => { + this.dispatchEvent(e); + if (e instanceof events.QUICSocketErrorEvent) { + this.dispatchEvent( + new events.QUICServerErrorEvent({ + detail: e.detail, + }), + ); + } + }; + + protected handleQUICConnectionEvents = (e: events.QUICConnectionEvent) => { + this.dispatchEvent(e); }; public constructor({ crypto, - socket, config, + socket, resolveHostname = utils.resolveHostname, reasonToCode, codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, keepaliveIntervalTime, logger, }: { crypto: { key: ArrayBuffer; - ops: Crypto; + ops: { + sign(key: ArrayBuffer, data: ArrayBuffer): Promise; + verify( + key: ArrayBuffer, + data: ArrayBuffer, + sig: ArrayBuffer, + ): Promise; + }; + }; + config: Partial & { + key: string | Array | Uint8Array | Array; + cert: string | Array | Uint8Array | Array; }; socket?: QUICSocket; - // This actually requires TLS - // You have to specify these some how - // We can force it - config: Partial & { tlsConfig: TlsConfig }; resolveHostname?: (hostname: Hostname) => Host | PromiseLike; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - maxReadableStreamBytes?: number; - maxWritableStreamBytes?: number; keepaliveIntervalTime?: number; logger?: Logger; }) { @@ -106,7 +120,6 @@ class QUICServer extends EventTarget { this.crypto = crypto; if (socket == null) { this.socket = new QUICSocket({ - crypto, resolveHostname, logger: this.logger.getChild(QUICSocket.name), }); @@ -122,8 +135,6 @@ class QUICServer extends EventTarget { this.config = quicConfig; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; - this.maxReadableStreamBytes = maxReadableStreamBytes; - this.maxWritableStreamBytes = maxWritableStreamBytes; this.keepaliveIntervalTime = keepaliveIntervalTime; } @@ -146,16 +157,17 @@ class QUICServer extends EventTarget { public async start({ host = '::' as Host, port = 0 as Port, + reuseAddr, }: { host?: Host | Hostname; port?: Port; + reuseAddr?: boolean; } = {}) { let address: string; if (!this.isSocketShared) { address = utils.buildAddress(host, port); this.logger.info(`Start ${this.constructor.name} on ${address}`); - await this.socket.start({ host, port }); - this.socket.addEventListener('error', this.handleQUICSocketError); + await this.socket.start({ host, port, reuseAddr }); address = utils.buildAddress(this.socket.host, this.socket.port); } else { // If the socket is shared, it must already be started @@ -165,6 +177,17 @@ class QUICServer extends EventTarget { address = utils.buildAddress(this.socket.host, this.socket.port); this.logger.info(`Start ${this.constructor.name} on ${address}`); } + + // Register on all socket events + this.socket.addEventListener( + 'socketError', + this.handleQUICSocketEvents + ); + this.socket.addEventListener( + 'socketStop', + this.handleQUICSocketEvents + ); + this.logger.info(`Started ${this.constructor.name} on ${address}`); } @@ -176,6 +199,7 @@ class QUICServer extends EventTarget { }: { force?: boolean; } = {}) { + // console.time('destroy conn'); const address = utils.buildAddress(this.socket.host, this.socket.port); this.logger.info(`Stop ${this.constructor.name} on ${address}`); const destroyProms: Array> = []; @@ -183,6 +207,7 @@ class QUICServer extends EventTarget { destroyProms.push(connection.destroy({ force })); } await Promise.all(destroyProms); + // console.timeEnd('destroy conn'); this.socket.deregisterServer(this); if (!this.isSocketShared) { // If the socket is not shared, then it can be stopped @@ -193,18 +218,44 @@ class QUICServer extends EventTarget { this.logger.info(`Stopped ${this.constructor.name} on ${address}`); } + // Because the `ctx` is not passed in from the outside + // It makes sense that this is only done during construction + // And importantly we just enable the cancellation of this + // Nothing else really + + /** + * This method must not throw any exceptions. + * Any errors must be emitted as events. + * @internal + */ public async connectionNew( - data: Buffer, remoteInfo: RemoteInfo, header: Header, dcid: QUICConnectionId, - scid: QUICConnectionId, ): Promise { - const peerAddress = utils.buildAddress(remoteInfo.host, remoteInfo.port); - if (header.ty !== quiche.Type.Initial) { - this.logger.debug(`QUIC packet must be Initial for new connections`); + + // If the packet is not an `Initial` nor `ZeroRTT` then we discard the + // packet. + if ( + header.ty !== quiche.Type.Initial && + header.ty !== quiche.Type.ZeroRTT + ) { return; } + + // Derive the new connection's SCID from the client generated DCID + const scid = new QUICConnectionId( + await this.crypto.ops.sign( + this.crypto.key, + dcid, + ), + 0, + quiche.MAX_CONN_ID_LEN, + ); + + const peerAddress = utils.buildAddress(remoteInfo.host, remoteInfo.port); + + // Version Negotiation if (!quiche.versionIsSupported(header.version)) { this.logger.debug( @@ -299,8 +350,6 @@ class QUICServer extends EventTarget { config: this.config, reasonToCode: this.reasonToCode, codeToReason: this.codeToReason, - maxReadableStreamBytes: this.maxReadableStreamBytes, - maxWritableStreamBytes: this.maxWritableStreamBytes, logger: this.logger.getChild( `${QUICConnection.name} ${scid.toString().slice(32)}-${clientConnRef}`, ), @@ -408,18 +457,7 @@ class QUICServer extends EventTarget { dcid: QUICConnectionId, peerHost: Host, ): Promise { - const msgData = { dcid: dcid.toString(), host: peerHost }; - const msgJSON = JSON.stringify(msgData); - const msgBuffer = Buffer.from(msgJSON); - const msgSig = Buffer.from( - await this.crypto.ops.sign(this.crypto.key, msgBuffer), - ); - const tokenData = { - msg: msgBuffer.toString('base64url'), - sig: msgSig.toString('base64url'), - }; - const tokenJSON = JSON.stringify(tokenData); - return Buffer.from(tokenJSON); + return utils.mintToken(dcid, peerHost, this.crypto); } /** @@ -433,42 +471,7 @@ class QUICServer extends EventTarget { tokenBuffer: Buffer, peerHost: Host, ): Promise { - let tokenData; - try { - tokenData = JSON.parse(tokenBuffer.toString()); - } catch { - return; - } - if (typeof tokenData !== 'object' || tokenData == null) { - return; - } - if ( - typeof tokenData.msg !== 'string' || - typeof tokenData.sig !== 'string' - ) { - return; - } - const msgBuffer = Buffer.from(tokenData.msg, 'base64url'); - const msgSig = Buffer.from(tokenData.sig, 'base64url'); - if (!(await this.crypto.ops.verify(this.crypto.key, msgBuffer, msgSig))) { - return; - } - let msgData; - try { - msgData = JSON.parse(msgBuffer.toString()); - } catch { - return; - } - if (typeof msgData !== 'object' || msgData == null) { - return; - } - if (typeof msgData.dcid !== 'string' || typeof msgData.host !== 'string') { - return; - } - if (msgData.host !== peerHost) { - return; - } - return QUICConnectionId.fromString(msgData.dcid); + return utils.validateToken(tokenBuffer, peerHost, this.crypto); } } diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index 6a9b84e2..80fb0a63 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -4,7 +4,7 @@ import type { Crypto, Host, Hostname, Port } from './types'; import type { Header } from './native/types'; import dgram from 'dgram'; import Logger from '@matrixai/logger'; -import { running, destroyed } from '@matrixai/async-init'; +import { status, running, destroyed } from '@matrixai/async-init'; import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; import QUICConnectionId from './QUICConnectionId'; import QUICConnectionMap from './QUICConnectionMap'; @@ -15,8 +15,8 @@ import * as errors from './errors'; /** * Events: - * - error - * - stop + * - socketError + * - socketStop */ interface QUICSocket extends StartStop {} @StartStop() @@ -37,9 +37,16 @@ class QUICSocket extends EventTarget { protected socketClose: () => Promise; protected socketSend: (...params: Array) => Promise; - protected crypto: { + protected crypto?: { key: ArrayBuffer; - ops: Crypto; + ops: { + sign(key: ArrayBuffer, data: ArrayBuffer): Promise; + verify( + key: ArrayBuffer, + data: ArrayBuffer, + sig: ArrayBuffer, + ): Promise; + }; }; /** @@ -62,84 +69,62 @@ class QUICSocket extends EventTarget { try { header = quiche.Header.fromSlice(data, quiche.MAX_CONN_ID_LEN); } catch (e) { - // `InvalidPacket` means that this is not a QUIC packet. - // If so, then we just ignore the packet. - if (e.message !== 'InvalidPacket') { - // Only emit an error if it is not an `InvalidPacket` error. - // Do note, that this kind of error is a peer error. - // The error is not due to us. + // `BufferTooShort` and `InvalidPacket` means that this is not a QUIC + // packet. If so, then we just ignore the packet. + if ( + e.message !== 'BufferTooShort' && + e.message !== 'InvalidPacket' + ) { + // Emit error if it is not a `BufferTooShort` or `InvalidPacket` error. + // This would indicate something went wrong in header parsing. + // This is not a critical error, but should be checked. this.dispatchEvent(new events.QUICSocketErrorEvent({ detail: e })); } return; } - - // Apparently if it is a UDP datagram - // it could be a QUIC datagram, and not part of any connection - // However I don't know how the DCID would work in a QUIC dgram - // We have not explored this yet - - // Destination Connection ID is the ID the remote peer chose for us. + // All QUIC packets will have the `dcid` header property + // However short packets will not have the `scid` property + // The destination connection ID is supposed to be our connection ID const dcid = new QUICConnectionId(header.dcid); - - // Derive our SCID using HMAC signing. - const scid = new QUICConnectionId( - await this.crypto.ops.sign( - this.crypto.key, - dcid, // <- use DCID (which is a copy), otherwise it will cause memory problems later in the NAPI - ), - 0, - quiche.MAX_CONN_ID_LEN, - ); - const remoteInfo_ = { host: remoteInfo.address as Host, port: remoteInfo.port as Port, }; - - // Now both must be checked - let conn: QUICConnection; - if (!this.connectionMap.has(dcid) && !this.connectionMap.has(scid)) { - // If a server is not registered - // then this packet is useless, and we can discard it + let connection: QUICConnection; + if (!this.connectionMap.has(dcid)) { + // If the DCID is not known, and the server has not been registered then + // we discard the packet> if (this.server == null) { return; } - const conn_ = await this.server.connectionNew( - data, + // At this point, the connection may not yet be started + const connection_ = await this.server.connectionNew( remoteInfo_, header, dcid, - scid, ); // If there's no connection yet - // Then the server is in the middle of the version negotiation/stateless retry - // or the handshake process - if (conn_ == null) { + // then the server is middle of version negotiation or stateless retry + if (connection_ == null) { return; } - conn = conn_; + connection = connection_; } else { - conn = this.connectionMap.get(dcid) ?? this.connectionMap.get(scid)!; - - // The connection may be a client or server connection - // When we register a client, we have to put the connection in our - // connection map + connection = this.connectionMap.get(dcid)!; } - await conn.recv(data, remoteInfo_); - - // The `conn.recv` now may actually destroy the connection - // In that sense, there's nothing to send - // That's the `conn.destroy` might call `conn.send` - // So it's all sent - // So we should only send things if it isn't already destroyed - // Remember that there is 3 possible events to the QUICConnection - // send, recv, timeout - // That's it. - // Each send/recv/timeout may result in a destruction - if (!conn[destroyed]) { - // Ignore any errors, concurrent with destruction - await conn.send().catch(() => {}); + // If the connection has already stopped running + // then we discard the packet. + if (!connection[running]) { + return; } + // Acquire the conn lock, this ensures mutual exclusion + // for state changes on the internal connection + await connection.connLock.withF(async () => { + // Even if we are `stopping`, the `quiche` library says we need to + // continue processing any packets. + connection.recv(data, remoteInfo_); + await connection.send(); + }); }; /** @@ -150,20 +135,14 @@ class QUICSocket extends EventTarget { }; public constructor({ - crypto, resolveHostname = utils.resolveHostname, logger, }: { - crypto: { - key: ArrayBuffer; - ops: Crypto; - }; resolveHostname?: (hostname: Hostname) => Host | PromiseLike; logger?: Logger; }) { super(); this.logger = logger ?? new Logger(this.constructor.name); - this.crypto = crypto; this.resolveHostname = resolveHostname; } @@ -207,10 +186,12 @@ class QUICSocket extends EventTarget { public async start({ host = '::' as Host, port = 0 as Port, + reuseAddr = false, ipv6Only = false, }: { host?: Host | Hostname; port?: Port; + reuseAddr?: boolean; ipv6Only?: boolean; } = {}): Promise { let address = utils.buildAddress(host, port); @@ -223,7 +204,7 @@ class QUICSocket extends EventTarget { ); this.socket = dgram.createSocket({ type: udpType, - reuseAddr: false, + reuseAddr, ipv6Only, }); this.socketBind = utils.promisify(this.socket.bind).bind(this.socket); @@ -276,7 +257,7 @@ class QUICSocket extends EventTarget { * If force is true, it will skip checking connections and stop the socket. * @param force - Will force the socket to end even if there are active connections, used for cleaning up after tests. */ - public async stop(force = false): Promise { + public async stop({ force = false }: { force?: boolean } = {}): Promise { const address = utils.buildAddress(this._host, this._port); this.logger.info(`Stop ${this.constructor.name} on ${address}`); if (!force && this.connectionMap.size > 0) { diff --git a/src/QUICStream.ts b/src/QUICStream.ts index 4bdf1de8..2c1ff305 100644 --- a/src/QUICStream.ts +++ b/src/QUICStream.ts @@ -7,7 +7,7 @@ import type { ConnectionMetadata, } from './types'; import type { Connection } from './native/types'; -import { ReadableStream, WritableStream } from 'stream/web'; +import { ReadableStream, WritableStream, ByteLengthQueuingStrategy } from 'stream/web'; import Logger from '@matrixai/logger'; import { CreateDestroy, @@ -21,7 +21,7 @@ import * as errors from './errors'; /** * Events: - * - destroy + * - streamDestroy * * Swap from using `readable` and `writable` to just function calls. * It's basically the same, since it's just the connection telling the stream @@ -68,16 +68,12 @@ class QUICStream reasonToCode = () => 0, codeToReason = (type, code) => new Error(`${type.toString()} ${code.toString()}`), - maxReadableStreamBytes = 100_000, // About 100KB - maxWritableStreamBytes = 100_000, // About 100KB logger = new Logger(`${this.name} ${streamId}`), }: { streamId: StreamId; connection: QUICConnection; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - maxReadableStreamBytes?: number; - maxWritableStreamBytes?: number; logger?: Logger; }): Promise { logger.info(`Create ${this.name}`); @@ -93,8 +89,6 @@ class QUICStream connection, reasonToCode, codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, logger, }); connection.streamMap.set(stream.streamId, stream); @@ -107,16 +101,12 @@ class QUICStream connection, reasonToCode, codeToReason, - maxReadableStreamBytes, - maxWritableStreamBytes, logger, }: { streamId: StreamId; connection: QUICConnection; reasonToCode: StreamReasonToCode; codeToReason: StreamCodeToReason; - maxReadableStreamBytes: number; - maxWritableStreamBytes: number; logger: Logger; }) { super(); @@ -142,9 +132,10 @@ class QUICStream await this.closeRecv(true, reason); }, }, - { - highWaterMark: maxReadableStreamBytes, - }, + new ByteLengthQueuingStrategy({ + highWaterMark: 0, + // highWaterMark: maxReadableStreamBytes, + }), ); this.writable = new WritableStream( @@ -181,9 +172,10 @@ class QUICStream await this.closeSend(true, reason); }, }, - { - highWaterMark: maxWritableStreamBytes, - }, + new ByteLengthQueuingStrategy({ + highWaterMark: 0, + // highWaterMark: maxWritableStreamBytes, + }), ); } diff --git a/src/config.ts b/src/config.ts index 58147d0e..7dca8a84 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,113 +1,163 @@ +import type { QUICConfig } from './types'; import type { Config as QuicheConfig } from './native/types'; import { quiche } from './native'; +import * as errors from './errors'; -// All the algos chrome supports + ed25519 -const supportedPrivateKeyAlgosDefault = - 'ed25519:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512'; - -export type TlsConfig = - | { - certChainPem: string | null; - privKeyPem: string | null; - } - | { - certChainFromPemFile: string | null; - privKeyFromPemFile: string | null; - }; - -type QUICConfig = { - tlsConfig: TlsConfig | undefined; - verifyPem: string | undefined; - verifyFromPemFile: string | undefined; - supportedPrivateKeyAlgos: string | undefined; - verifyPeer: boolean; - logKeys: string | undefined; - grease: boolean; - maxIdleTimeout: number; - maxRecvUdpPayloadSize: number; - maxSendUdpPayloadSize: number; - initialMaxData: number; - initialMaxStreamDataBidiLocal: number; - initialMaxStreamDataBidiRemote: number; - initialMaxStreamsBidi: number; - initialMaxStreamsUni: number; - disableActiveMigration: boolean; - applicationProtos: string[]; - enableEarlyData: boolean; -}; +/** + * BoringSSL does not support: + * - rsa_pss_pss_sha256 + * - rsa_pss_pss_sha384 + * - rsa_pss_pss_sha512 + * - ed448 + */ +const sigalgs = [ + 'rsa_pkcs1_sha256', + 'rsa_pkcs1_sha384', + 'rsa_pkcs1_sha512', + 'rsa_pss_rsae_sha256', + 'rsa_pss_rsae_sha384', + 'rsa_pss_rsae_sha512', + 'ecdsa_secp256r1_sha256', + 'ecdsa_secp384r1_sha384', + 'ecdsa_secp521r1_sha512', + 'ed25519', +].join(':'); const clientDefault: QUICConfig = { - tlsConfig: undefined, - verifyPem: undefined, - verifyFromPemFile: undefined, - supportedPrivateKeyAlgos: supportedPrivateKeyAlgosDefault, - logKeys: undefined, + sigalgs, verifyPeer: true, grease: true, - maxIdleTimeout: 5000, - maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, - maxSendUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, - initialMaxData: 10000000, - initialMaxStreamDataBidiLocal: 1000000, - initialMaxStreamDataBidiRemote: 1000000, + maxIdleTimeout: 0, + maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 + maxSendUdpPayloadSize: quiche.MIN_CLIENT_INITIAL_LEN, // 1200, + initialMaxData: 10 * 1024 * 1024, + initialMaxStreamDataBidiLocal: 1 * 1024 * 1024, + initialMaxStreamDataBidiRemote: 1 * 1024 * 1024, + initialMaxStreamDataUni: 1 * 1024 * 1024, initialMaxStreamsBidi: 100, initialMaxStreamsUni: 100, + enableDgram: [false, 0, 0], disableActiveMigration: true, - applicationProtos: ['http/0.9'], + applicationProtos: ['quic'], enableEarlyData: true, }; const serverDefault: QUICConfig = { - tlsConfig: undefined, - verifyPem: undefined, - verifyFromPemFile: undefined, - supportedPrivateKeyAlgos: supportedPrivateKeyAlgosDefault, - logKeys: undefined, + sigalgs, verifyPeer: false, grease: true, - maxIdleTimeout: 5000, - maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, - maxSendUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, - initialMaxData: 10000000, - initialMaxStreamDataBidiLocal: 1000000, - initialMaxStreamDataBidiRemote: 1000000, + maxIdleTimeout: 0, + maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 + maxSendUdpPayloadSize: quiche.MIN_CLIENT_INITIAL_LEN, // 1200 + initialMaxData: 10 * 1024 * 1024, + initialMaxStreamDataBidiLocal: 1 * 1024 * 1024, + initialMaxStreamDataBidiRemote: 1 * 1024 * 1024, + initialMaxStreamDataUni: 1 * 1024 * 1024, initialMaxStreamsBidi: 100, initialMaxStreamsUni: 100, + enableDgram: [false, 0, 0], disableActiveMigration: true, - applicationProtos: ['http/0.9'], + applicationProtos: ['quic'], enableEarlyData: true, }; +const textDecoder = new TextDecoder('utf-8'); +const textEncoder = new TextEncoder(); + +/** + * Converts QUICConfig to QuicheConfig. + * This does not use all the options of QUICConfig. + * The QUICConfig is still necessary. + */ function buildQuicheConfig(config: QUICConfig): QuicheConfig { - let certChainPem: Buffer | null = null; - let privKeyPem: Buffer | null = null; - if (config.tlsConfig != null && 'certChainPem' in config.tlsConfig) { - if (config.tlsConfig.certChainPem != null) { - certChainPem = Buffer.from(config.tlsConfig.certChainPem); + if (config.key != null && config.cert == null) { + throw new errors.ErrorQUICConfig( + 'The cert option must be set when key is set', + ); + } else if (config.key == null && config.cert != null) { + throw new errors.ErrorQUICConfig( + 'The key option must be set when cert is set', + ); + } else if (config.key != null && config.cert != null) { + if (Array.isArray(config.key) && Array.isArray(config.cert)) { + if (config.key.length !== config.cert.length) { + throw new errors.ErrorQUICConfig( + 'The number of keys must match the number of certs', + ); + } } - if (config.tlsConfig.privKeyPem != null) { - privKeyPem = Buffer.from(config.tlsConfig.privKeyPem); + } + // This is a concatenated CA certificates in PEM format + let caPEMBuffer: Uint8Array | undefined; + if (config.ca != null) { + let caPEMString = ''; + if (typeof config.ca === 'string') { + caPEMString = config.ca.trim() + '\n'; + } else if (config.ca instanceof Uint8Array) { + caPEMString = textDecoder.decode(config.ca).trim() + '\n'; + } else if (Array.isArray(config.ca)) { + for (const c of config.ca) { + if (typeof c === 'string') { + caPEMString += c.trim() + '\n'; + } else { + caPEMString += textDecoder.decode(c).trim() + '\n'; + } + } } + caPEMBuffer = textEncoder.encode(caPEMString); } - const quicheConfig: QuicheConfig = quiche.Config.withBoringSslCtx( - certChainPem, - privKeyPem, - config.supportedPrivateKeyAlgos ?? null, - config.verifyPem != null ? Buffer.from(config.verifyPem) : null, - config.verifyPeer, - ); - if (config.tlsConfig != null && 'certChainFromPemFile' in config.tlsConfig) { - if (config.tlsConfig?.certChainFromPemFile != null) { - quicheConfig.loadCertChainFromPemFile( - config.tlsConfig.certChainFromPemFile, - ); + // This is an array of private keys in PEM format + let keyPEMBuffers: Array | undefined; + if (config.key != null) { + const keyPEMs: Array = []; + if (typeof config.key === 'string') { + keyPEMs.push(config.key.trim() + '\n'); + } else if (config.key instanceof Uint8Array) { + keyPEMs.push(textDecoder.decode(config.key).trim() + '\n'); + } else if (Array.isArray(config.key)) { + for (const k of config.key) { + if (typeof k === 'string') { + keyPEMs.push(k.trim() + '\n'); + } else { + keyPEMs.push(textDecoder.decode(k).trim() + '\n'); + } + } } - if (config.tlsConfig?.privKeyFromPemFile != null) { - quicheConfig.loadPrivKeyFromPemFile(config.tlsConfig.privKeyFromPemFile); + keyPEMBuffers = keyPEMs.map((k) => textEncoder.encode(k)); + } + // This is an array of certificate chains in PEM format + let certChainPEMBuffers: Array | undefined; + if (config.cert != null) { + const certChainPEMs: Array = []; + if (typeof config.cert === 'string') { + certChainPEMs.push(config.cert.trim() + '\n'); + } else if (config.cert instanceof Uint8Array) { + certChainPEMs.push(textDecoder.decode(config.cert).trim() + '\n'); + } else if (Array.isArray(config.cert)) { + for (const c of config.cert) { + if (typeof c === 'string') { + certChainPEMs.push(c.trim() + '\n'); + } else { + certChainPEMs.push(textDecoder.decode(c).trim() + '\n'); + } + } } + certChainPEMBuffers = certChainPEMs.map((c) => textEncoder.encode(c)); } - if (config.verifyFromPemFile != null) { - quicheConfig.loadVerifyLocationsFromFile(config.verifyFromPemFile); + let quicheConfig: QuicheConfig; + try { + quicheConfig = quiche.Config.withBoringSslCtx( + config.verifyPeer, + caPEMBuffer, + keyPEMBuffers, + certChainPEMBuffers, + config.sigalgs, + ); + } catch (e) { + throw new errors.ErrorQUICConfig( + `Failed to build Quiche config with custom SSL context: ${e.message}`, + { cause: e } + ); } if (config.logKeys != null) { quicheConfig.logKeys(); @@ -126,13 +176,13 @@ function buildQuicheConfig(config: QUICConfig): QuicheConfig { quicheConfig.setInitialMaxStreamDataBidiRemote( config.initialMaxStreamDataBidiRemote, ); + quicheConfig.setInitialMaxStreamDataUni(config.initialMaxStreamDataUni); quicheConfig.setInitialMaxStreamsBidi(config.initialMaxStreamsBidi); quicheConfig.setInitialMaxStreamsUni(config.initialMaxStreamsUni); + quicheConfig.enableDgram(...config.enableDgram); quicheConfig.setDisableActiveMigration(config.disableActiveMigration); quicheConfig.setApplicationProtos(config.applicationProtos); return quicheConfig; } export { clientDefault, serverDefault, buildQuicheConfig }; - -export type { QUICConfig }; diff --git a/src/errors.ts b/src/errors.ts index 8917183a..6429b49a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,9 +1,14 @@ +import type { POJO } from '@matrixai/errors'; import { AbstractError } from '@matrixai/errors'; class ErrorQUIC extends AbstractError { static description = 'QUIC error'; } +class ErrorQUICConfig extends ErrorQUIC { + static description = 'QUIC config error'; +} + class ErrorQUICSocket extends ErrorQUIC { static description = 'QUIC Socket error'; } @@ -45,6 +50,10 @@ class ErrorQUICClient extends ErrorQUIC { static description = 'QUIC Client error'; } +class ErrorQUICClientCreateTimeOut extends ErrorQUICClient { + static description = 'QUICC Client create timeout'; +} + class ErrorQUICClientDestroyed extends ErrorQUICClient { static description = 'QUIC Client is destroyed'; } @@ -62,16 +71,45 @@ class ErrorQUICConnection extends ErrorQUIC { static description = 'QUIC Connection error'; } -class ErrorQUICConnectionDestroyed extends ErrorQUICConnection { - static description = 'QUIC Connection is destroyed'; +class ErrorQUICConnectionNotRunning extends ErrorQUICConnection { + static description = 'QUIC Connection is not running'; +} + +class ErrorQUICConnectionStartTimeOut extends ErrorQUICConnection { + static description = 'QUIC Connection start timeout'; } -class ErrorQUICConnectionTimeout extends ErrorQUICConnection { +/** + * Quiche does not create a local or peer error during idle timeout. + */ +class ErrorQUICConnectionIdleTimeOut extends ErrorQUICConnection { static description = 'QUIC Connection reached idle timeout'; } -class ErrorQUICConnectionTLSFailure extends ErrorQUICConnection { - static description = 'QUIC connection had failure with TLS negotiation'; +/** + * These errors arise from the internal quiche connection. + * These can be local errors (as in the case of TLS verification failure). + * Or they can be remote errors. + * If the connection fails to verify the peer, it will be a local error. + * The error code might be 304. + * You may want a "cause" though? + * But it's not always a cause + * Plus it might be useless + * New ErrorQUICConnectionInternal('TlsFail', { data: {}) + * Note that the reason can be buffer. + * Which means it does not need to be a reason + * + * Note that TlsFail error codes are documented here: + * https://github.com/google/boringssl/blob/master/include/openssl/ssl.h + */ +class ErrorQUICConnectionInternal extends ErrorQUICConnection { + static description = 'QUIC Connection internal conn error'; + public data: { + type: 'local' | 'remote'; + isApp: boolean; + errorCode: number; + reason: Uint8Array; + } & POJO; } class ErrorQUICStream extends ErrorQUIC { @@ -105,6 +143,7 @@ class ErrorQUICUndefinedBehaviour extends ErrorQUIC { export { ErrorQUIC, + ErrorQUICConfig, ErrorQUICSocket, ErrorQUICSocketNotRunning, ErrorQUICSocketServerDuplicate, @@ -115,13 +154,15 @@ export { ErrorQUICServerNotRunning, ErrorQUICServerSocketNotRunning, ErrorQUICClient, + ErrorQUICClientCreateTimeOut, ErrorQUICClientDestroyed, ErrorQUICClientSocketNotRunning, ErrorQUICClientInvalidHost, ErrorQUICConnection, - ErrorQUICConnectionDestroyed, - ErrorQUICConnectionTimeout, - ErrorQUICConnectionTLSFailure, + ErrorQUICConnectionNotRunning, + ErrorQUICConnectionStartTimeOut, + ErrorQUICConnectionIdleTimeOut, + ErrorQUICConnectionInternal, ErrorQUICStream, ErrorQUICStreamDestroyed, ErrorQUICStreamLocked, diff --git a/src/events.ts b/src/events.ts index 47132f03..0c42da9a 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,9 +1,19 @@ import type QUICConnection from './QUICConnection'; import type QUICStream from './QUICStream'; +// Socket events + +abstract class QUICSocketEvent extends Event {}; + +class QUICSocketStartEvent extends Event { + constructor(options?: EventInit) { + super('socketStart', options); + } +} + class QUICSocketStopEvent extends Event { constructor(options?: EventInit) { - super('stop', options); + super('socketStop', options); } } @@ -14,11 +24,37 @@ class QUICSocketErrorEvent extends Event { detail: Error; }, ) { - super('error', options); + super('socketError', options); + this.detail = options.detail; + } +} + +// Client events + +abstract class QUICClientEvent extends Event {}; + +class QUICClientDestroyEvent extends Event { + constructor(options?: EventInit) { + super('clientDestroy', options); + } +} + +class QUICClientErrorEvent extends Event { + public detail: Error; + constructor( + options: EventInit & { + detail: Error; + }, + ) { + super('clientError', options); this.detail = options.detail; } } +// Server events + +abstract class QUICServerEvent extends Event {}; + class QUICServerConnectionEvent extends Event { public detail: QUICConnection; constructor( @@ -26,14 +62,20 @@ class QUICServerConnectionEvent extends Event { detail: QUICConnection; }, ) { - super('connection', options); + super('serverConnection', options); this.detail = options.detail; } } +class QUICServerStartEvent extends Event { + constructor(options?: EventInit) { + super('serverStart', options); + } +} + class QUICServerStopEvent extends Event { constructor(options?: EventInit) { - super('stop', options); + super('serverStop', options); } } @@ -44,113 +86,82 @@ class QUICServerErrorEvent extends Event { detail: QUICSocketErrorEvent | Error; }, ) { - super('error', options); + super('serverError', options); this.detail = options.detail; } } -// The stream, may come with initial information -// So we can put the remote info regarding that -// Into the stream -// Each stream will have a fixed conn ID -// Then again... it's possible for the stream to have varying peer info too -// It sort of depends again -// Since you could be handling 1 stream -// Then another stream runs, concurrently from a different remote host -// So you never really get the initial thing - -class QUICConnectionStreamEvent extends Event { +// Connection events + +abstract class QUICConnectionEvent extends Event {}; + +class QUICConnectionStreamEvent extends QUICConnectionEvent { public detail: QUICStream; constructor( options: EventInit & { detail: QUICStream; }, ) { - super('stream', options); + super('connectionStream', options); this.detail = options.detail; } } -class QUICConnectionSendEvent extends Event { +class QUICConnectionStartEvent extends QUICConnectionEvent { constructor(options?: EventInit) { - super('send', options); + super('connectionStart', options); } } -class QUICConnectionRecvEvent extends Event { +class QUICConnectionStopEvent extends QUICConnectionEvent { constructor(options?: EventInit) { - super('recv', options); + super('connectionStop', options); } } -class QUICConnectionDestroyEvent extends Event { - constructor(options?: EventInit) { - super('destroy', options); - } -} - -class QUICConnectionErrorEvent extends Event { +class QUICConnectionErrorEvent extends QUICConnectionEvent { public detail: Error; constructor( options: EventInit & { detail: Error; }, ) { - super('error', options); + super('connectionError', options); this.detail = options.detail; } } -class QUICStreamReadableEvent extends Event { - constructor(options?: EventInit) { - super('readable', options); - } -} - -class QUICStreamWritableEvent extends Event { - constructor(options?: EventInit) { - super('writable', options); - } -} +// Stream events -class QUICStreamDestroyEvent extends Event { - constructor(options?: EventInit) { - super('destroy', options); - } -} +abstract class QUICStreamEvent extends Event {}; -class QUICClientDestroyEvent extends Event { +class QUICStreamDestroyEvent extends QUICStreamEvent { constructor(options?: EventInit) { - super('destroy', options); - } -} - -class QUICClientErrorEvent extends Event { - public detail: QUICSocketErrorEvent | QUICConnectionErrorEvent; - constructor( - options: EventInit & { - detail: QUICSocketErrorEvent | QUICConnectionErrorEvent; - }, - ) { - super('error', options); - this.detail = options.detail; + super('streamDestroy', options); } } export { + QUICSocketEvent, + QUICSocketStartEvent, QUICSocketStopEvent, QUICSocketErrorEvent, + QUICClientEvent, + QUICClientDestroyEvent, + QUICClientErrorEvent, + + QUICServerEvent, QUICServerConnectionEvent, + QUICServerStartEvent, QUICServerStopEvent, QUICServerErrorEvent, + + QUICConnectionEvent, QUICConnectionStreamEvent, - QUICConnectionSendEvent, - QUICConnectionRecvEvent, - QUICConnectionDestroyEvent, + QUICConnectionStartEvent, + QUICConnectionStopEvent, QUICConnectionErrorEvent, - QUICStreamReadableEvent, - QUICStreamWritableEvent, + + QUICStreamEvent, QUICStreamDestroyEvent, - QUICClientDestroyEvent, - QUICClientErrorEvent, }; diff --git a/src/native/napi/config.rs b/src/native/napi/config.rs index bb200391..b8272cd8 100644 --- a/src/native/napi/config.rs +++ b/src/native/napi/config.rs @@ -1,4 +1,3 @@ -// use core::panicking::panic; use napi_derive::napi; use napi::bindgen_prelude::*; @@ -41,39 +40,76 @@ impl Config { let config = quiche::Config::new( quiche::PROTOCOL_VERSION ).or_else( - |err| Err(Error::from_reason(err.to_string())) + |err| Err(napi::Error::from_reason(err.to_string())) )?; return Ok(Config(config)); } + /// Creates configuration with custom TLS context + /// Servers must be setup with a key and cert #[napi(factory)] pub fn with_boring_ssl_ctx( - cert_pem: Option, - key_pem: Option, - supported_key_algos: Option, - ca_cert_pem: Option, verify_peer: bool, + ca: Option, + key: Option>, + cert: Option>, + sigalgs: Option, ) -> Result { let mut ssl_ctx_builder = boring::ssl::SslContextBuilder::new( boring::ssl::SslMethod::tls(), ).or_else( - |err| Err(Error::from_reason(err.to_string())) + |e| Err(napi::Error::from_reason(e.to_string())) )?; - let verify_value = if verify_peer {boring::ssl::SslVerifyMode::PEER | boring::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT } - else { boring::ssl::SslVerifyMode::NONE }; + let verify_value = if verify_peer { + boring::ssl::SslVerifyMode::PEER | boring::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT + } else { + boring::ssl::SslVerifyMode::NONE + }; ssl_ctx_builder.set_verify(verify_value); - // Processing and adding the cert chain - if let Some(cert_pem) = cert_pem { - let x509_cert_chain = boring::x509::X509::stack_from_pem( - &cert_pem.to_vec() + // Setup all CA certificates + if let Some(ca) = ca { + let mut x509_store_builder = boring::x509::store::X509StoreBuilder::new() + .or_else( + |e| Err(napi::Error::from_reason(e.to_string())) + )?; + let x509_certs = boring::x509::X509::stack_from_pem( + &ca.to_vec() ).or_else( + |e| Err(napi::Error::from_reason(e.to_string())) + )?; + for x509 in x509_certs.into_iter() { + x509_store_builder.add_cert(x509) + .or_else( + |e| Err(napi::Error::from_reason(e.to_string())) + )?; + } + let x509_store = x509_store_builder.build(); + ssl_ctx_builder.set_verify_cert_store(x509_store) + .or_else( + |e| Err(napi::Error::from_reason(e.to_string())) + )?; + } + // Setup all certificates and keys + if let (Some(key), Some(cert)) = (key, cert) { + // Right now the boring crate does not provide a straight forward way of + // setting multiple independent certificate chains. So we are just picking + // the first key and cert pair. + let (k, c) = (key[0].to_vec(), cert[0].to_vec()); + let private_key = boring::pkey::PKey::private_key_from_pem(&k) + .or_else( |err| Err(Error::from_reason(err.to_string())) )?; + ssl_ctx_builder.set_private_key(&private_key).or_else( + |e| Err(napi::Error::from_reason(e.to_string())) + )?; + let x509_cert_chain = boring::x509::X509::stack_from_pem( + &c + ).or_else( + |err| Err(napi::Error::from_reason(err.to_string())) + )?; for (i, cert) in x509_cert_chain.iter().enumerate() { if i == 0 { - ssl_ctx_builder.set_certificate( - cert, - ).or_else( + ssl_ctx_builder.set_certificate(cert,).or_else( |err| Err(Error::from_reason(err.to_string())) )?; } else { @@ -85,53 +121,18 @@ impl Config { } } } - // Processing and adding the private key - if let Some(key_pem) = key_pem { - let private_key = boring::pkey::PKey::private_key_from_pem(&key_pem) - .or_else( - |err| Err(Error::from_reason(err.to_string())) - )?; - ssl_ctx_builder.set_private_key(&private_key) - .or_else( - |err| Err(Error::from_reason(err.to_string())) - )?; - } - // Adding supported private key algorithms - if let Some(supported_key_algos) = supported_key_algos { - ssl_ctx_builder.set_sigalgs_list(&supported_key_algos) - .or_else( - |err| Err(Error::from_reason(err.to_string())) - )?; - } - // Processing CA certificate - if let Some(ca_cert_pem) = ca_cert_pem { - let x509_certs = boring::x509::X509::stack_from_pem( - &ca_cert_pem.to_vec() - ).or_else( - |err| Err(Error::from_reason(err.to_string())) + // Setup supported signature algorithms + if let Some(sigalgs) = sigalgs { + ssl_ctx_builder.set_sigalgs_list(&sigalgs).or_else( + |e| Err(napi::Error::from_reason(e.to_string())) )?; - let mut x509_store_builder = boring::x509::store::X509StoreBuilder::new() - .or_else( - |err| Err(Error::from_reason(err.to_string())) - )?; - for x509 in x509_certs.into_iter() { - x509_store_builder.add_cert(x509) - .or_else( - |err| Err(Error::from_reason(err.to_string())) - )?; - } - let x509_store = x509_store_builder.build(); - ssl_ctx_builder.set_verify_cert_store(x509_store) - .or_else( - |err| Err(Error::from_reason(err.to_string())) - )?; } let ssl_ctx= ssl_ctx_builder.build(); let config = quiche::Config::with_boring_ssl_ctx( quiche::PROTOCOL_VERSION, ssl_ctx, ).or_else( - |err| Err(Error::from_reason(err.to_string())) + |e| Err(Error::from_reason(e.to_string())) )?; return Ok(Config(config)); } diff --git a/src/native/napi/connection.rs b/src/native/napi/connection.rs index 3d70f845..26e3a1c2 100644 --- a/src/native/napi/connection.rs +++ b/src/native/napi/connection.rs @@ -36,7 +36,7 @@ pub enum ConnectionErrorCode { pub struct ConnectionError { pub is_app: bool, pub error_code: i64, - pub reason: Vec, + pub reason: Uint8Array, } impl From for ConnectionError { @@ -44,7 +44,7 @@ impl From for ConnectionError { return ConnectionError { is_app: err.is_app, error_code: err.error_code as i64, - reason: err.reason.to_vec(), + reason: Uint8Array::new(err.reason), }; } } @@ -1030,7 +1030,10 @@ impl Connection { #[napi] pub fn is_closed(&self) -> bool { - return self.0.is_closed(); + // eprintln!("RUST: CALLING IS_CLOSED"); + let x = self.0.is_closed(); + // eprintln!("RUST: FINISH CALLING IS_CLOSED======="); + return x; } #[napi] diff --git a/src/native/types.ts b/src/native/types.ts index f6c3b20c..b7f42693 100644 --- a/src/native/types.ts +++ b/src/native/types.ts @@ -44,11 +44,11 @@ interface Config { interface ConfigConstructor { new (): Config; withBoringSslCtx( - certPem: Uint8Array | null, - keyPem: Uint8Array | null, - supportedKeyAlgos: string | null, - ca_cert_pem: Uint8Array | null, - verify_peer: boolean, + verifyPeer: boolean, + ca?: Uint8Array | undefined | null, + key?: Array | undefined | null, + cert?: Array | undefined | null, + sigalgs?: string | undefined | null, ): Config; } @@ -205,7 +205,7 @@ enum ConnectionErrorCode { type ConnectionError = { isApp: boolean; errorCode: number; - reason: Array; + reason: Uint8Array; }; type Stats = { diff --git a/src/types.ts b/src/types.ts index 75dd4303..8f386eb5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,14 @@ +import type { Timer } from '@matrixai/timer'; import type QUICStream from './QUICStream'; +type ContextCancellable = { + signal: AbortSignal; +}; + +type ContextTimed = ContextCancellable & { + timer: Timer; +}; + /** * Opaque types are wrappers of existing types * that require smart constructors @@ -96,6 +105,193 @@ type ConnectionMetadata = { remotePort: Port; }; +type QUICConfig = { + /** + * Certificate authority certificate in PEM format or Uint8Array buffer + * containing PEM formatted certificate. Each string or Uint8Array can be + * one certificate or multiple certificates concatenated together. The order + * does not matter, each is an independent certificate authority. Multiple + * concatenated certificate authorities can be passed. They are all + * concatenated together. + * + * When this is not set, this defaults to the operating system's CA + * certificates. OpenSSL (and forks of OpenSSL) all support the + * environment variables `SSL_CERT_DIR` and `SSL_CERT_FILE`. + */ + ca?: string | Array | Uint8Array | Array; + + /** + * Private key as a PEM string or Uint8Array buffer containing PEM formatted + * key. You can pass multiple keys. The number of keys must match the number + * of certs. Each key must be associated to the the corresponding cert chain. + * + * Currently multiple key and certificate chains is not supported. + */ + key?: string | Array | Uint8Array | Array; + + /** + * X.509 certificate chain in PEM format or Uint8Array buffer containing + * PEM formatted certificate chain. Each string or Uint8Array is a + * certificate chain in subject to issuer order. Multiple certificate chains + * can be passed. The number of certificate chains must match the number of + * keys. Each certificate chain must be associated to the corresponding key. + * + * Currently multiple key and certificate chains is not supported. + */ + cert?: string | Array | Uint8Array | Array; + + /** + * Colon separated list of supported signature algorithms. + * + * When this is not set, this defaults to the following list: + * - rsa_pkcs1_sha256 + * - rsa_pkcs1_sha384 + * - rsa_pkcs1_sha512 + * - rsa_pss_rsae_sha256 + * - rsa_pss_rsae_sha384 + * - rsa_pss_rsae_sha512 + * - ecdsa_secp256r1_sha256 + * - ecdsa_secp384r1_sha384 + * - ecdsa_secp521r1_sha512 + * - ed25519 + */ + sigalgs?: string; + + /** + * Verify the other peer. + * Clients by default set this to true. + * Servers by default set this to false. + */ + verifyPeer: boolean; + + /** + * Enables the logging of secret keys to a file path. + * Use this with wireshark to decrypt the QUIC packets for debugging. + * This defaults to undefined. + */ + logKeys?: string; + + /** + * Enable "Generate Random extensions and Sustain Extensibilty". + * This prevents protocol ossification by periodically introducing + * random no-op values in the optional fields in TLS. + * This defaults to true. + */ + grease: boolean; + + /** + * This controls the interval for keeping alive an idle connection. + * This time will be used to send a ping frame to keep the connection alive. + * This is only useful if the `maxIdleTimeout` is set to greater than 0. + * This is defaulted to `undefined`. + * This is not a quiche option. + */ + keepAliveIntervalTime?: number; + + /** + * Maximum number of milliseconds to wait for an idle connection. + * If this time is exhausted with no answer from the peer, then + * the connection will timeout. This applies to any open connection. + * Note that the QUIC client will repeatedly send initial packets to + * a non-responding QUIC server up to this time. + * This is defaulted to `0` meaning infinite time. + */ + maxIdleTimeout: number; + + /** + * Maximum incoming UDP payload size. + * The maximum IPv4 UDP payload size is 65507. + * The maximum IPv6 UDP payload size is 65527. + * This is defaulted to 65527. + */ + maxRecvUdpPayloadSize: number; + + /** + * Maximum outgoing UDP payload size. + * + * It is advantageous to set this size to be lower than the maximum + * transmission unit size, which is commonly set to 1500. + * This is defaulted 1200. It is also the minimum. + */ + maxSendUdpPayloadSize: number; + + /** + * Maximum buffer size of incoming stream data for an entire connection. + * If set to 0, then no incoming stream data is allowed, therefore setting + * to 0 effectively disables incoming stream data. + * This defaults to 10 MiB. + */ + initialMaxData: number; + + /** + * Maximum buffer size of incoming stream data for a locally initiated + * bidirectional stream. This is the buffer size for a single stream. + * If set to 0, this disables incoming stream data for locally initiated + * bidirectional streams. + * This defaults to 1 MiB. + */ + initialMaxStreamDataBidiLocal: number; + + /** + * Maximum buffer size of incoming stream data for a remotely initiated + * bidirectional stream. This is the buffer size for a single stream. + * If set to 0, this disables incoming stream data for remotely initiated + * bidirectional streams. + * This defaults to 1 MiB. + */ + initialMaxStreamDataBidiRemote: number; + + /** + * Maximum buffer size of incoming stream data for a remotely initiated + * unidirectional stream. This is the buffer size for a single stream. + * If set to 0, this disables incoming stream data for remotely initiated + * unidirectional streams. + * This defaults to 1 MiB. + */ + initialMaxStreamDataUni: number; + + /** + * Maximum number of remotely initiated bidirectional streams. + * A bidirectional stream is closed once all incoming data is read up to the + * fin offset or when the stream's read direction is shutdown and all + * outgoing data is acked by the peer up to the fin offset or when the + * stream's write direction is shutdown. + * This defaults to 100. + */ + initialMaxStreamsBidi: number; + + /** + * Maximum number of remotely initiated unidirectional streams. + * A unidirectional stream is closed once all incoming data is read up to the + * fin offset or when the stream's read direction is shutdown. + * This defaults to 100. + */ + initialMaxStreamsUni: number; + + /** + * Enables receiving dgram. + * The 2 numbers are receive queue length and send queue length. + * This defaults to `[false, 0, 0]`. + */ + enableDgram: [boolean, number, number]; + + disableActiveMigration: boolean; + + /** + * Application protocols is necessary for ALPN. + * This is must be non-empty, otherwise there will be a + * `NO_APPLICATION_PROTOCOL` error. + * Choose from: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids + * For HTTP3, use `['h3', 'h3-29', 'h3-28', 'h3-27']`. + * Both the client and server must share the ALPN in order to establish a + * connection. + * This defaults to `['quic']` as a placeholder ALPN. + */ + applicationProtos: string[]; + + enableEarlyData: boolean; +}; + export type { Opaque, Callback, @@ -113,4 +309,7 @@ export type { StreamReasonToCode, StreamCodeToReason, ConnectionMetadata, + QUICConfig, + ContextCancellable, + ContextTimed, }; diff --git a/src/utils.ts b/src/utils.ts index b4a0e38a..a7cb7a14 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,9 @@ import type { ConnectionIdString, Host, Hostname, + Crypto } from './types'; +import QUICConnectionId from './QUICConnectionId'; import dns from 'dns'; import { IPv4, IPv6, Validator } from 'ip-num'; import * as errors from './errors'; @@ -318,6 +320,78 @@ function certificatePEMsToCertChainPem(pems: Array): string { return certChainPEM; } +async function mintToken( + dcid: QUICConnectionId, + peerHost: Host, + crypto:{ + key: ArrayBuffer; + ops: Crypto; + } +): Promise { + const msgData = { dcid: dcid.toString(), host: peerHost }; + const msgJSON = JSON.stringify(msgData); + const msgBuffer = Buffer.from(msgJSON); + const msgSig = Buffer.from( + await crypto.ops.sign(crypto.key, msgBuffer), + ); + const tokenData = { + msg: msgBuffer.toString('base64url'), + sig: msgSig.toString('base64url'), + }; + const tokenJSON = JSON.stringify(tokenData); + return Buffer.from(tokenJSON); +} + +async function validateToken( + tokenBuffer: Buffer, + peerHost: Host, + crypto: { + key: ArrayBuffer; + ops: Crypto; + } +): Promise { + let tokenData; + try { + tokenData = JSON.parse(tokenBuffer.toString()); + } catch { + return; + } + if (typeof tokenData !== 'object' || tokenData == null) { + return; + } + if ( + typeof tokenData.msg !== 'string' || + typeof tokenData.sig !== 'string' + ) { + return; + } + const msgBuffer = Buffer.from(tokenData.msg, 'base64url'); + const msgSig = Buffer.from(tokenData.sig, 'base64url'); + if (!(await crypto.ops.verify(crypto.key, msgBuffer, msgSig))) { + return; + } + let msgData; + try { + msgData = JSON.parse(msgBuffer.toString()); + } catch { + return; + } + if (typeof msgData !== 'object' || msgData == null) { + return; + } + if (typeof msgData.dcid !== 'string' || typeof msgData.host !== 'string') { + return; + } + if (msgData.host !== peerHost) { + return; + } + return QUICConnectionId.fromString(msgData.dcid); +} + +async function sleep(ms: number): Promise { + return await new Promise((r) => setTimeout(r, ms)); +} + export { isIPv4, isIPv6, @@ -341,4 +415,7 @@ export { never, certificateDERToPEM, certificatePEMsToCertChainPem, + mintToken, + validateToken, + sleep, }; diff --git a/test-bootstrap.ts b/test-bootstrap.ts new file mode 100644 index 00000000..274882f5 --- /dev/null +++ b/test-bootstrap.ts @@ -0,0 +1,174 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { QUICConfig, Crypto, Host, Hostname, Port } from './src/types'; +import dgram from 'dgram'; +import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; +import QUICServer from './src/QUICServer'; +import QUICConnectionId from './src/QUICConnectionId'; +import QUICConnection from './src/QUICConnection'; +import QUICSocket from './src/QUICSocket'; +import { clientDefault, buildQuicheConfig } from './src/config'; +import { quiche } from './src/native'; +import * as utils from './src/utils'; +import * as testsUtils from './tests/utils'; + + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certECDSA: X509Certificate; + let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; + }; + let certECDSAPEM: string; + let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certEd25519: X509Certificate; + let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; + }; + let certEd25519PEM: string; + +async function main() { + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + + // This has to be setup asynchronously due to key generation + let crypto: { + key: ArrayBuffer; + ops: Crypto; + }; + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + const logger = new Logger(`${QUICServer.name} Test`, LogLevel.ERROR, [ new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + ), + ]); + + const quicServer = new QUICServer({ + crypto, + config: { + // key: keyPairRSAPEM.privateKey, + // cert: certRSAPEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + verifyPeer: false + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: '127.0.0.1' as Host + }); + + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + const scid = new QUICConnectionId(scidBuffer); + + // Verify peer + // Note that you cannot send to IPv4 from dual stack socket + // It must be sent as IPv4 mapped IPv6 + + const socket = new QUICSocket({ + crypto, + logger: logger.getChild(QUICSocket.name), + }); + await socket.start({ + host: '127.0.0.1' as Host + }); + + // ??? + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false + }; + + // This creates a connection state + // We now need to trigger it + const connection = await QUICConnection.connectQUICConnection({ + scid, + socket, + remoteInfo: { + host: quicServer.host, + port: quicServer.port, + }, + config: clientConfig, + logger: logger.getChild(QUICConnection.name + '--CLIENT CONNECTION--') + }); + + connection.addEventListener('error', (e) => { + console.log('error', e); + }); + + // Trigger the connection + await connection.send(); + + // wait till it is established + // console.log('BEFORE ESTABLISHED P'); + await connection.establishedP; + // console.log('AFTER ESTABLISHED P'); + + // You must destroy the connection + // console.log('DESTROY CONNECTION'); + await connection.destroy(); + // console.log('DESTROYED CONNECTION'); + + + // Due to idle timer? + + await socket.stop(); + + console.log('<<<< STARTED TO SLEEP'); + await testsUtils.sleep(100000); + console.log('<<<< FINISHED TO SLEEP'); + + // console.log('STOP SOCKET'); + console.time('STOP SERVER'); + await quicServer.stop(); + console.timeEnd('STOP SERVER'); +} + +void main(); diff --git a/test-client-conn.ts b/test-client-conn.ts new file mode 100644 index 00000000..e157f409 --- /dev/null +++ b/test-client-conn.ts @@ -0,0 +1,136 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { QUICConfig, Crypto, Host, Hostname, Port } from './src/types'; +import dgram from 'dgram'; +import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; +import QUICServer from './src/QUICServer'; +import QUICConnectionId from './src/QUICConnectionId'; +// import QUICConnection from './src/QUICConnection'; +import QUICSocket from './src/QUICSocket'; +import { clientDefault, buildQuicheConfig } from './src/config'; +import { quiche } from './src/native'; +import * as utils from './src/utils'; +import * as testsUtils from './tests/utils'; +import QUICConnectionClient from './src/QUICConnectionClient'; + +let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}; +let certRSA: X509Certificate; +let keyPairRSAPEM: { + publicKey: string; + privateKey: string; +}; +let certRSAPEM: string; +let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}; +let certECDSA: X509Certificate; +let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; +}; +let certECDSAPEM: string; +let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}; +let certEd25519: X509Certificate; +let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; +}; +let certEd25519PEM: string; + +async function main() { + + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + + // This has to be setup asynchronously due to key generation + let crypto: { + key: ArrayBuffer; + ops: Crypto; + }; + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + const logger = new Logger(`${QUICServer.name} Test`, LogLevel.ERROR, [new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + )]); + + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + const scid = new QUICConnectionId(scidBuffer); + + const socket = new QUICSocket({ + crypto, + logger: logger.getChild(QUICSocket.name), + }); + await socket.start({ + host: '127.0.0.1' as Host + }); + + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false + }; + + const connClient = await QUICConnectionClient.connectQUICConnection({ + scid, + socket, + remoteInfo: { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }, + config: clientConfig, + logger: logger.getChild('--CLIENT CONNECTION--') + }); + + await testsUtils.sleep(1000); + + console.log(connClient.conn.timeout()); + + + await connClient.destroy(); + await socket.stop(); + + + + + + +} + +void main(); diff --git a/test-keygen-webcrypto.ts b/test-keygen-webcrypto.ts new file mode 100644 index 00000000..3d3a05fc --- /dev/null +++ b/test-keygen-webcrypto.ts @@ -0,0 +1,443 @@ +import type { X509Certificate } from '@peculiar/x509'; +import { Crypto } from '@peculiar/webcrypto'; +import * as x509 from '@peculiar/x509'; + +/** + * WebCrypto polyfill from @peculiar/webcrypto + * This behaves differently with respect to Ed25519 keys + * See: https://github.com/PeculiarVentures/webcrypto/issues/55 + */ +const webcrypto = new Crypto(); + +/** + * Monkey patches the global crypto object polyfill + */ +globalThis.crypto = webcrypto; + +x509.cryptoProvider.set(webcrypto); + +async function generateKeyPairRSA(): Promise<{ + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}> { + const keyPair = await webcrypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: 'SHA-256' + }, + true, + [ + 'sign', + 'verify', + ] + ); + return { + publicKey: await webcrypto.subtle.exportKey( + 'jwk', + keyPair.publicKey + ), + privateKey: await webcrypto.subtle.exportKey( + 'jwk', + keyPair.privateKey + ) + }; +} + +async function generateKeyPairECDSA(): Promise<{ + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}> { + const keyPair = await webcrypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + true, + [ + 'sign', + 'verify' + ] + ); + return { + publicKey: await webcrypto.subtle.exportKey( + 'jwk', + keyPair.publicKey + ), + privateKey: await webcrypto.subtle.exportKey( + 'jwk', + keyPair.privateKey + ) + }; +} + +async function generateKeyPairEd25519(): Promise<{ + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}> { + const keyPair = await webcrypto.subtle.generateKey( + { + name: 'EdDSA', + namedCurve: 'Ed25519' + }, + true, + [ + 'sign', + 'verify' + ] + ) as CryptoKeyPair; + return { + publicKey: await webcrypto.subtle.exportKey( + 'jwk', + keyPair.publicKey + ), + privateKey: await webcrypto.subtle.exportKey( + 'jwk', + keyPair.privateKey + ) + }; +} + +/** + * Imports public key. + * This uses `@peculiar/webcrypto` API for Ed25519 keys. + */ +async function importPublicKey(publicKey: JsonWebKey): Promise { + let algorithm; + switch (publicKey.kty) { + case 'RSA': + switch(publicKey.alg) { + case 'RS256': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }; + break; + case 'RS384': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-384' + }; + break; + case 'RS512': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-512' + }; + break; + default: + throw new Error(`Unsupported algorithm ${publicKey.alg}`); + } + break; + case 'EC': + switch(publicKey.crv) { + case 'P-256': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-256', + }; + break; + case 'P-384': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-384', + }; + break; + case 'P-521': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-521', + }; + break; + default: + throw new Error(`Unsupported curve ${publicKey.crv}`); + } + break; + case 'OKP': + algorithm = { + name: 'EdDSA', + namedCurve: 'Ed25519', + }; + break; + default: + throw new Error(`Unsupported key type ${publicKey.kty}`); + } + return await webcrypto.subtle.importKey( + 'jwk', + publicKey, + algorithm, + true, + ['verify'] + ); +} + +/** + * Imports private key. + * This uses `@peculiar/webcrypto` API for Ed25519 keys. + */ +async function importPrivateKey(privateKey: JsonWebKey): Promise { + let algorithm; + switch (privateKey.kty) { + case 'RSA': + switch(privateKey.alg) { + case 'RS256': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }; + break; + case 'RS384': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-384' + }; + break; + case 'RS512': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-512' + }; + break; + default: + throw new Error(`Unsupported algorithm ${privateKey.alg}`); + } + break; + case 'EC': + switch(privateKey.crv) { + case 'P-256': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-256', + }; + break; + case 'P-384': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-384', + }; + break; + case 'P-521': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-521', + }; + break; + default: + throw new Error(`Unsupported curve ${privateKey.crv}`); + } + break; + case 'OKP': + algorithm = { + name: 'EdDSA', + namedCurve: 'Ed25519', + }; + break; + default: + throw new Error(`Unsupported key type ${privateKey.kty}`); + } + return await webcrypto.subtle.importKey( + 'jwk', + privateKey, + algorithm, + true, + ['sign'] + ); +} + +const extendedKeyUsageFlags = { + serverAuth: '1.3.6.1.5.5.7.3.1', + clientAuth: '1.3.6.1.5.5.7.3.2', + codeSigning: '1.3.6.1.5.5.7.3.3', + emailProtection: '1.3.6.1.5.5.7.3.4', + timeStamping: '1.3.6.1.5.5.7.3.8', + ocspSigning: '1.3.6.1.5.5.7.3.9', +}; + +/** + * Generate x509 certificate. + * Duration is in seconds. + * X509 certificates currently use `UTCTime` format for `notBefore` and `notAfter`. + * This means: + * - Only second resolution. + * - Minimum date for validity is 1970-01-01T00:00:00Z (inclusive). + * - Maximum date for valdity is 2049-12-31T23:59:59Z (inclusive). + */ +async function generateCertificate({ + certId, + subjectKeyPair, + issuerPrivateKey, + duration, + subjectAttrsExtra = [], + issuerAttrsExtra = [], + now = new Date(), +}: { + certId: string; + subjectKeyPair: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + issuerPrivateKey: JsonWebKey; + duration: number; + subjectAttrsExtra?: Array<{ [key: string]: Array }>; + issuerAttrsExtra?: Array<{ [key: string]: Array }>; + now?: Date; +}): Promise { + const certIdNum = parseInt(certId); + const iss = certIdNum === 0 ? certIdNum : certIdNum - 1; + const sub = certIdNum; + const subjectPublicCryptoKey = await importPublicKey( + subjectKeyPair.publicKey, + ); + const subjectPrivateCryptoKey = await importPrivateKey( + subjectKeyPair.privateKey, + ); + const issuerPrivateCryptoKey = await importPrivateKey(issuerPrivateKey); + if (duration < 0) { + throw new RangeError('`duration` must be positive'); + } + // X509 `UTCTime` format only has resolution of seconds + // this truncates to second resolution + const notBeforeDate = new Date(now.getTime() - (now.getTime() % 1000)); + const notAfterDate = new Date(now.getTime() - (now.getTime() % 1000)); + // If the duration is 0, then only the `now` is valid + notAfterDate.setSeconds(notAfterDate.getSeconds() + duration); + if (notBeforeDate < new Date(0)) { + throw new RangeError( + '`notBeforeDate` cannot be before 1970-01-01T00:00:00Z', + ); + } + if (notAfterDate > new Date(new Date('2050').getTime() - 1)) { + throw new RangeError('`notAfterDate` cannot be after 2049-12-31T23:59:59Z'); + } + const serialNumber = certId; + // The entire subject attributes and issuer attributes + // is constructed via `x509.Name` class + // By default this supports on a limited set of names: + // CN, L, ST, O, OU, C, DC, E, G, I, SN, T + // If custom names are desired, this needs to change to constructing + // `new x509.Name('FOO=BAR', { FOO: '1.2.3.4' })` manually + // And each custom attribute requires a registered OID + // Because the OID is what is encoded into ASN.1 + const subjectAttrs = [ + { + CN: [`${sub}`], + }, + // Filter out conflicting CN attributes + ...subjectAttrsExtra.filter((attr) => !('CN' in attr)), + ]; + const issuerAttrs = [ + { + CN: [`${iss}`], + }, + // Filter out conflicting CN attributes + ...issuerAttrsExtra.filter((attr) => !('CN' in attr)), + ]; + const signingAlgorithm: any = issuerPrivateCryptoKey.algorithm; + if (signingAlgorithm.name === 'ECDSA') { + switch(signingAlgorithm.namedCurve) { + case 'P-256': + signingAlgorithm.hash = 'SHA-256'; + break; + case 'P-384': + signingAlgorithm.hash = 'SHA-384'; + break; + case 'P-521': + signingAlgorithm.hash = 'SHA-512'; + break; + default: + throw new TypeError( + `Issuer private key has an unsupported curve: ${signingAlgorithm.namedCurve}` + ); + } + } + const certConfig = { + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + subject: subjectAttrs, + issuer: issuerAttrs, + signingAlgorithm, + publicKey: subjectPublicCryptoKey, + signingKey: subjectPrivateCryptoKey, + extensions: [ + new x509.BasicConstraintsExtension(true, undefined, true), + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.nonRepudiation | + x509.KeyUsageFlags.keyAgreement | + x509.KeyUsageFlags.keyEncipherment | + x509.KeyUsageFlags.dataEncipherment, + true, + ), + new x509.ExtendedKeyUsageExtension([ + extendedKeyUsageFlags.serverAuth, + extendedKeyUsageFlags.clientAuth, + extendedKeyUsageFlags.codeSigning, + extendedKeyUsageFlags.emailProtection, + extendedKeyUsageFlags.timeStamping, + extendedKeyUsageFlags.ocspSigning, + ]), + await x509.SubjectKeyIdentifierExtension.create(subjectPublicCryptoKey), + ] as Array, + }; + certConfig.signingKey = issuerPrivateCryptoKey; + return await x509.X509CertificateGenerator.create(certConfig); +} + +async function main() { + const keyPairRSA = await generateKeyPairRSA(); + const keyPairECDSA = await generateKeyPairECDSA(); + const keyPairEd25519 = await generateKeyPairEd25519(); + + console.log(keyPairRSA); + console.log(keyPairECDSA); + console.log(keyPairEd25519); + + const publicKeyRSA = await importPublicKey(keyPairRSA.publicKey); + const publicKeyECDSA = await importPublicKey(keyPairECDSA.publicKey); + const publicKeyEd25519 = await importPublicKey(keyPairEd25519.publicKey); + + console.log(publicKeyRSA); + console.log(publicKeyECDSA); + console.log(publicKeyEd25519); + + const privateKeyRSA = await importPrivateKey(keyPairRSA.privateKey); + const privateKeyECDSA = await importPrivateKey(keyPairECDSA.privateKey); + const privateKeyEd25519 = await importPrivateKey(keyPairEd25519.privateKey); + + console.log(privateKeyRSA); + console.log(privateKeyECDSA); + console.log(privateKeyEd25519); + + + const certRSA = await generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + + const certECDSA = await generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + + const certEd25519 = await generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + + console.log(certRSA); + console.log(certECDSA); + console.log(certEd25519); + +} + +void main(); diff --git a/test-keygen.ts b/test-keygen.ts new file mode 100644 index 00000000..d035cae9 --- /dev/null +++ b/test-keygen.ts @@ -0,0 +1,80 @@ +import type { JsonWebKey } from 'crypto'; +import crypto from 'crypto'; + +async function generateKeyPairRSA(): Promise<{ + publicKey: JsonWebKey, + privateKey: JsonWebKey +}> { + return new Promise((resolve, reject) => { + crypto.generateKeyPair('rsa', { + modulusLength: 2048, + }, (err, publicKey, privateKey) => { + if (err) { + reject(err); + } else { + resolve({ + publicKey: publicKey.export({ format: 'jwk' }), + privateKey: privateKey.export({ format: 'jwk' }), + }); + } + }); + }); +} + +async function generateKeyPairECDSA(): Promise<{ + publicKey: JsonWebKey, + privateKey: JsonWebKey, +}> { + return new Promise((resolve, reject) => { + crypto.generateKeyPair('ec', { + namedCurve: 'P-256', + }, (err, publicKey, privateKey) => { + if (err) { + reject(err); + } else { + resolve({ + publicKey: publicKey.export({ + format: 'jwk' + }), + privateKey: privateKey.export({ + format: 'jwk' + }) + }); + } + }); + }); +} + +async function generateKeyPairEd25519(): Promise<{ + publicKey: JsonWebKey, + privateKey: JsonWebKey +}> { + return new Promise((resolve, reject) => { + crypto.generateKeyPair( + 'ed25519', + undefined, + (e, publicKey, privateKey) => { + if (e) { + reject(e); + } else { + resolve({ + publicKey: publicKey.export({ + format: 'jwk' + }), + privateKey: privateKey.export({ + format: 'jwk' + }) + }); + } + } + ); + }); +} + +async function main() { + console.log(await generateKeyPairRSA()); + console.log(await generateKeyPairECDSA()); + console.log(await generateKeyPairEd25519()); +} + +void main(); diff --git a/tests/QUICConnectionId.test.ts b/tests/QUICConnectionId.test.ts new file mode 100644 index 00000000..5e72e8ae --- /dev/null +++ b/tests/QUICConnectionId.test.ts @@ -0,0 +1,28 @@ +import QUICConnectionId from '@/QUICConnectionId'; +import { quiche } from '@/native'; +import * as testsUtils from './utils'; + +describe(QUICConnectionId.name, () => { + test('connection ID is a Uint8Array', async () => { + const cidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await testsUtils.randomBytes(cidBuffer); + const cid = new QUICConnectionId(cidBuffer); + expect(cid).toBeInstanceOf(Uint8Array); + }); + test('connection ID encode to hex string and decode from hex string', async () => { + const cidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await testsUtils.randomBytes(cidBuffer); + const cid = new QUICConnectionId(cidBuffer); + const cidString = cid.toString(); + const cid_ = QUICConnectionId.fromString(cidString); + expect(cid).toEqual(cid_); + }); + test('connection ID to buffer and from buffer is zero-copy', async () => { + const cidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await testsUtils.randomBytes(cidBuffer); + const cid = new QUICConnectionId(cidBuffer); + expect(cid.toBuffer().buffer).toBe(cidBuffer); + const cid_ = QUICConnectionId.fromBuffer(cid.toBuffer()); + expect(cid_.buffer).toBe(cidBuffer); + }); +}); diff --git a/tests/QUICServer.test.ts b/tests/QUICServer.test.ts new file mode 100644 index 00000000..2b8e3041 --- /dev/null +++ b/tests/QUICServer.test.ts @@ -0,0 +1,384 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { QUICConfig, Crypto, Host, Hostname, Port } from '@/types'; +import dgram from 'dgram'; +import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; +import QUICServer from '@/QUICServer'; +import QUICConnectionId from '@/QUICConnectionId'; +import QUICConnection from '@/QUICConnection'; +import QUICSocket from '@/QUICSocket'; +import { clientDefault, buildQuicheConfig } from '@/config'; +import { quiche } from '@/native'; +import * as utils from '@/utils'; +import * as testsUtils from './utils'; + +describe(QUICServer.name, () => { + const logger = new Logger(`${QUICServer.name} Test`, LogLevel.WARN, [ new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + ), + ]); + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certECDSA: X509Certificate; + let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; + }; + let certECDSAPEM: string; + let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certEd25519: X509Certificate; + let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; + }; + let certEd25519PEM: string; + beforeAll(async () => { + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + }); + // This has to be setup asynchronously due to key generation + let crypto: { + key: ArrayBuffer; + ops: Crypto; + }; + beforeEach(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + }); + describe('start and stop', () => { + test('with RSA', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start(); + // Default to dual-stack + expect(quicServer.host).toBe('::'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('with ECDSA', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start(); + // Default to dual-stack + expect(quicServer.host).toBe('::'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('with Ed25519', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start(); + // Default to dual-stack + expect(quicServer.host).toBe('::'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + }); + describe('binding to host and port', () => { + test('listen on IPv4', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: '127.0.0.1' as Host + }); + expect(quicServer.host).toBe('127.0.0.1'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('listen on IPv6', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: '::1' as Host + }); + expect(quicServer.host).toBe('::1'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('listen on dual stack', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: '::' as Host + }); + expect(quicServer.host).toBe('::'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('listen on IPv4 mapped IPv6', async () => { + // NOT RECOMMENDED, because send addresses will have to be mapped + // addresses, which means you can ONLY connect to mapped addresses + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: '::ffff:127.0.0.1' as Host + }); + expect(quicServer.host).toBe('::ffff:127.0.0.1'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + await quicServer.start({ + host: '::ffff:7f00:1' as Host + }); + // Will resolve to dotted-decimal variant + expect(quicServer.host).toBe('::ffff:127.0.0.1'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('listen on hostname', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: 'localhost' as Hostname + }); + // Default to using dns lookup, which uses the OS DNS resolver + const host = await utils.resolveHostname('localhost' as Hostname); + expect(quicServer.host).toBe(host); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + test('listen on hostname and custom resolver', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }, + resolveHostname: () => '127.0.0.1' as Host, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: 'abcdef' as Hostname + }); + expect(quicServer.host).toBe('127.0.0.1'); + expect(typeof quicServer.port).toBe('number'); + await quicServer.stop(); + }); + }); + describe.only('connection bootstrap', () => { + // Test without peer verification + test.only('', async () => { + const quicServer = new QUICServer({ + crypto, + config: { + // key: keyPairRSAPEM.privateKey, + // cert: certRSAPEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + verifyPeer: false + }, + logger: logger.getChild('QUICServer'), + }); + await quicServer.start({ + host: '127.0.0.1' as Host + }); + + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + const scid = new QUICConnectionId(scidBuffer); + + // Verify peer + // Note that you cannot send to IPv4 from dual stack socket + // It must be sent as IPv4 mapped IPv6 + + const socket = new QUICSocket({ + crypto, + logger: logger.getChild(QUICSocket.name), + }); + await socket.start({ + host: '127.0.0.1' as Host + }); + + // ??? + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false + }; + + // This creates a connection state + // We now need to trigger it + const connection = await QUICConnection.connectQUICConnection({ + scid, + socket, + remoteInfo: { + host: quicServer.host, + port: quicServer.port, + }, + config: clientConfig, + logger: logger.getChild(QUICConnection.name) + }); + + connection.addEventListener('error', (e) => { + console.log('error', e); + }); + + // Trigger the connection + await connection.send(); + + // wait till it is established + console.log('BEFORE ESTABLISHED P'); + await connection.establishedP; + console.log('AFTER ESTABLISHED P'); + + // You must destroy the connection + console.log('DESTROY CONNECTION'); + await connection.destroy(); + console.log('DESTROYED CONNECTION'); + + console.log('STOP SOCKET'); + await socket.stop(); + console.time('STOPPED SOCKET'); + await quicServer.stop(); + console.timeEnd('STOPPED SOCKET'); + }); + }); + // test('bootstrapping a new connection', async () => { + // const quicServer = new QUICServer({ + // crypto, + // config: { + // key: keyPairEd25519PEM.privateKey, + // cert: certEd25519PEM, + // }, + // logger: logger.getChild('QUICServer'), + // }); + // await quicServer.start(); + + // const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + // await crypto.ops.randomBytes(scidBuffer); + // const scid = new QUICConnectionId(scidBuffer); + + // const socket = new QUICSocket({ + // crypto, + // resolveHostname: utils.resolveHostname, + // logger: logger.getChild(QUICSocket.name), + // }); + // await socket.start(); + + // // Const config = buildQuicheConfig({ + // // ...clientDefault + // // }); + // // Here we want to VERIFY the peer + // // If we use the same certificate + // // then it should be consider as if it is trusted! + + // const quicConfig: QUICConfig = { + // ...clientDefault, + // verifyPeer: true, + // }; + + // const connection = await QUICConnection.connectQUICConnection({ + // scid, + // socket, + + // remoteInfo: { + // host: utils.resolvesZeroIP(quicServer.host), + // port: quicServer.port, + // }, + + // config: quicConfig, + // }); + + // await socket.stop(); + // await quicServer.stop(); + + // // We can run with several rsa keypairs and certificates + // }); + describe('updating configuration', () => { + // We want to test changing the configuration over time + }); + // Test hole punching, there's an initiation function + // We can make it start doing this, but technically it's the socket's duty to do this + // not just the server side +}); diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 00000000..27f1686c --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,301 @@ +import type { X509Certificate } from '@peculiar/x509'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import * as errors from '@/errors'; +import * as testsUtils from './utils'; + +describe('config', () => { + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certECDSA: X509Certificate; + let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; + }; + let certECDSAPEM: string; + let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certEd25519: X509Certificate; + let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; + }; + let certEd25519PEM: string; + beforeAll(async () => { + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + }); + test('build default client config', () => { + const config = buildQuicheConfig(clientDefault); + expect(config).toBeDefined() + }); + test('build default server config', () => { + const config = buildQuicheConfig(serverDefault); + expect(config).toBeDefined(); + }); + test('build with incorrect configuration', () => { + expect( + () => buildQuicheConfig({ + ...serverDefault, + sigalgs: 'ed448' + }) + ).toThrow(errors.ErrorQUICConfig); + expect( + () => buildQuicheConfig({ + ...serverDefault, + key: [keyPairRSAPEM.privateKey, keyPairECDSAPEM.privateKey], + cert: [certRSAPEM], + }) + ).toThrow(errors.ErrorQUICConfig); + }); + test('build with self-signed certificates', () => { + buildQuicheConfig({ + ...clientDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }); + buildQuicheConfig({ + ...clientDefault, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + }); + buildQuicheConfig({ + ...clientDefault, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }); + buildQuicheConfig({ + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }); + buildQuicheConfig({ + ...serverDefault, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + }); + buildQuicheConfig({ + ...serverDefault, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }); + }); + test('build with issued certificates', async () => { + const keyPairParent = await testsUtils.generateKeyPairEd25519(); + const certParent = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairParent, + issuerPrivateKey: keyPairParent.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const certParentPEM = testsUtils.certToPEM(certParent); + const keyPairChild = await testsUtils.generateKeyPairECDSA(); + const certChild = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairChild, + issuerPrivateKey: keyPairParent.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const keyPairChildPEM = await testsUtils.keyPairECDSAToPEM(keyPairChild); + const certChildPEM = testsUtils.certToPEM(certChild); + buildQuicheConfig({ + ...serverDefault, + ca: certParentPEM, + key: keyPairChildPEM.privateKey, + cert: certChildPEM, + }); + buildQuicheConfig({ + ...clientDefault, + ca: certParentPEM, + key: keyPairChildPEM.privateKey, + cert: certChildPEM, + }); + }); + test('build with multiple certificate authorities', async () => { + const keyPairCA1 = await testsUtils.generateKeyPairRSA(); + const keyPairCA2 = await testsUtils.generateKeyPairECDSA(); + const keyPairCA3 = await testsUtils.generateKeyPairEd25519(); + const caCert1 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairCA1, + issuerPrivateKey: keyPairCA1.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const caCert2 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairCA2, + issuerPrivateKey: keyPairCA2.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const caCert3 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairCA3, + issuerPrivateKey: keyPairCA3.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const caCert1PEM = testsUtils.certToPEM(caCert1); + const caCert2PEM = testsUtils.certToPEM(caCert2); + const caCert3PEM = testsUtils.certToPEM(caCert3); + buildQuicheConfig({ + ...clientDefault, + ca: [caCert1PEM, caCert2PEM, caCert3PEM], + verifyPeer: true, + }); + buildQuicheConfig({ + ...serverDefault, + ca: [caCert1PEM, caCert2PEM, caCert3PEM], + verifyPeer: true, + }); + }); + test('build with certificate chain', async () => { + const keyPairRoot = await testsUtils.generateKeyPairEd25519(); + const certRoot = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRoot, + issuerPrivateKey: keyPairRoot.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const keyPairIntermediate = await testsUtils.generateKeyPairEd25519(); + const certIntermediate = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairIntermediate, + issuerPrivateKey: keyPairRoot.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + + const keyPairLeaf = await testsUtils.generateKeyPairEd25519(); + const certLeaf = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairLeaf, + issuerPrivateKey: keyPairIntermediate.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const certRootPEM = testsUtils.certToPEM(certRoot); + const certIntermediatePEM = testsUtils.certToPEM(certIntermediate); + const certLeafPEM = testsUtils.certToPEM(certLeaf); + // These PEMs already have `\n` at the end + const certChainPEM = [certLeafPEM, certIntermediatePEM].join(''); + const keyPairLeafPEM = await testsUtils.keyPairEd25519ToPEM(keyPairLeaf); + buildQuicheConfig({ + ...clientDefault, + ca: certRootPEM, + key: keyPairLeafPEM.privateKey, + cert: certChainPEM, + }); + buildQuicheConfig({ + ...serverDefault, + ca: certRootPEM, + key: keyPairLeafPEM.privateKey, + cert: certChainPEM, + }); + }); + /** + * This currently is not supported. + * But the test will pass. + */ + test('build with multiple certificate chains', async () => { + const keyPairParent = await testsUtils.generateKeyPairEd25519(); + const certParent = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairParent, + issuerPrivateKey: keyPairParent.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const certParentPEM = testsUtils.certToPEM(certParent); + const keyPair1 = await testsUtils.generateKeyPairRSA(); + const keyPairPEM1 = await testsUtils.keyPairRSAToPEM(keyPair1); + const keyPair2 = await testsUtils.generateKeyPairECDSA(); + const keyPairPEM2 = await testsUtils.keyPairECDSAToPEM(keyPair2); + const keyPair3 = await testsUtils.generateKeyPairEd25519(); + const keyPairPEM3 = await testsUtils.keyPairEd25519ToPEM(keyPair3); + const cert1 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPair1, + issuerPrivateKey: keyPairParent.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const cert2 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPair2, + issuerPrivateKey: keyPairParent.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const cert3 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPair3, + issuerPrivateKey: keyPairParent.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + const certPEM1 = testsUtils.certToPEM(cert1); + const certPEM2 = testsUtils.certToPEM(cert2); + const certPEM3 = testsUtils.certToPEM(cert3); + buildQuicheConfig({ + ...clientDefault, + ca: certParentPEM, + key: [ + keyPairPEM1.privateKey, + keyPairPEM2.privateKey, + keyPairPEM3.privateKey + ], + cert: [ + certPEM1, + certPEM2, + certPEM3 + ], + verifyPeer: true, + }); + buildQuicheConfig({ + ...serverDefault, + ca: certParentPEM, + key: [ + keyPairPEM1.privateKey, + keyPairPEM2.privateKey, + keyPairPEM3.privateKey + ], + cert: [ + certPEM1, + certPEM2, + certPEM3 + ], + verifyPeer: true, + }); + }); +}); diff --git a/tests/native/quiche.connection.lifecycle.test.ts b/tests/native/quiche.connection.lifecycle.test.ts new file mode 100644 index 00000000..066658e8 --- /dev/null +++ b/tests/native/quiche.connection.lifecycle.test.ts @@ -0,0 +1,2209 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { QUICConfig, Crypto, Host, Hostname, Port } from '@/types'; +import type { Config, Connection, SendInfo } from '@/native/types'; +import { quiche } from '@/native'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import * as utils from '@/utils'; +import * as testsUtils from '../utils'; + +describe('quiche connection lifecycle', () => { + let crypto: { + key: ArrayBuffer; + ops: Crypto; + }; + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certECDSA: X509Certificate; + let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; + }; + let certECDSAPEM: string; + let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certEd25519: X509Certificate; + let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; + }; + let certEd25519PEM: string; + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + }); + describe('connection lifecycle', () => { + describe('connect and close client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + let clientQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('Hello World')); + expect(clientConn.peerError()).toBeNull(); + // According to RFC9000, if the connection is not in a position + // to send the connection close frame, then the local error + // is changed to be a protocol level error with the `ApplicationError` + // code and a cleared reason. + // If this connection was in a position to send the error, then + // we would expect the `isApp` to be `true`. + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: quiche.ConnectionErrorCode.ApplicationError, + reason: new Uint8Array() + }); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Client connection is closed (this is not true if there is draining) + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('after client close', async () => { + const randomPacketBuffer = new ArrayBuffer(1000); + await testsUtils.randomBytes(randomPacketBuffer); + const randomPacket = new Uint8Array(randomPacketBuffer); + // Random packets are received after the connection is closed + // However they are just dropped automatically + clientConn.recv( + randomPacket, + { + to: clientHost, + from: serverHost + } + ); + // You can receive multiple times without any problems + clientConn.recv( + randomPacket, + { + to: clientHost, + from: serverHost + } + ); + clientConn.recv( + randomPacket, + { + to: clientHost, + from: serverHost + } + ); + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(clientConn.isClosed()).toBeTrue(); + }); + }); + describe('connection timeouts', () => { + describe('dialing timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000 + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000 + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing timeout', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Exahust the timeout + await testsUtils.waitForTimeoutNull(clientConn); + // Connection has timed out + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Connection is closed + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + // No errors during idle timeout + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toBeNull(); + }); + }); + describe('initial timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000 + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000 + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + expect(serverConn.timeout()).toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // Once an idle max timeout is set, this timeout is no longer null + // Either the client or server or both can set the idle timeout + expect(serverConn.timeout()).not.toBeNull(); + }); + test('client <-initial- server timeout', async () => { + // Server tries sending the initial frame + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(serverConn.isTimedOut()).toBeFalse(); + // Let's assume the initial frame never gets received by the client + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isTimedOut()).toBeTrue(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + }); + describe('handshake timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000 + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000 + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + expect(serverConn.timeout()).toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // Once an idle max timeout is set, this timeout is no longer null + // Either the client or server or both can set the idle timeout + expect(serverConn.timeout()).not.toBeNull(); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-handshake- server timeout', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(serverConn.isTimedOut()).toBeFalse(); + // Let's assume the handshake frame never gets received by the client + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isTimedOut()).toBeTrue(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + }); + describe('established timeout', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + maxIdleTimeout: 2000 + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + maxIdleTimeout: 2000 + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + expect(serverConn.timeout()).toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // Once an idle max timeout is set, this timeout is no longer null + // Either the client or server or both can set the idle timeout + expect(serverConn.timeout()).not.toBeNull(); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> sever', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server timeout', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(serverConn.isTimedOut()).toBeFalse(); + // Let's assume the handshake frame never gets received by the client + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isTimedOut()).toBeTrue(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeFalse(); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.isTimedOut()).toBeTrue(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeTrue(); + expect(clientConn.isDraining()).toBeFalse(); + }); + }); + }); + describe('connection between client and server with RSA', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client dialing', async () => { + // Send the initial packet + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + // The initial frame will always be 1200 bytes + expect(clientSendLength).toBe(1200); + expect(clientSendInfo.from).toEqual(clientHost); + expect(clientSendInfo.to).toEqual(serverHost); + // This is the initial delay for the dialing procedure + // Quiche will repeatedly send the initial packet until it is received + // or exhausted the idle timeout, which in this case is 0 (disabled) + expect(typeof clientConn.timeout()!).toBe('number'); + // The initial delay starts at roughly 1 second + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(1000, -3); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + // Repeating send will throw `Done` + // This proves that only 1 send is necessary at the beginning + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Wait out the delay (add 50ms for non-determinism) + await testsUtils.sleep(clientConn.timeout()! + 50); + // Connection has not timed out because idle timeout defaults to infinity + expect(clientConn.isTimedOut()).toBeFalse(); + // The delay is exhausted, and therefore should be 0 + expect(clientConn.timeout()).toBe(0); + // The `onTimeout` must be called to transition state + clientConn.onTimeout(); + // The delay is repeated immediately after `onTimeout` + // It is still 1 second + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(1000, -3); + // Retry the initial packet + const clientBuffer_ = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer_); + expect(clientSendLength).toBe(1200); + expect(clientSendInfo.from).toEqual(clientHost); + expect(clientSendInfo.to).toEqual(serverHost); + // Retried initial frame is not an exact copy + expect(clientBuffer_).not.toEqual(clientBuffer); + // Upon the retry, the delay now doubles + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(2000, -3); + // This dialing process will repeat max idle timeout is exhausted + // Copy sendBuffer_ into sendBuffer + clientBuffer.set(clientBuffer_); + }); + test('client and server negotiation', async () => { + // Process the initial frame + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // It will be an initial packet + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + // The SCID is what was generated above + expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual(clientScid); + // The DCID is randomly generated by the client + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + expect(clientDcid).not.toEqual(clientScid); + // The token will be empty + expect(clientHeaderInitial.token).toHaveLength(0); + // The version should be 1 + expect(clientHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); + expect(clientHeaderInitial.versions).toBeNull(); + // Version negotiation + // The version is supported, we don't need to change + expect(quiche.versionIsSupported(clientHeaderInitial.version)).toBeTrue(); + // Derives a new SCID by signing the client's generated DCID + // This is only used during the stateless retry + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + const timeoutBeforeRecv = clientConn.timeout(); + const serverHeaderRetry = quiche.Header.fromSlice( + retryDatagram.subarray(0, retryDatagramLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderRetry.ty).toBe(quiche.Type.Retry); + // Retry packet's SCID is the derived SCID + expect(new QUICConnectionId(serverHeaderRetry.scid)).toEqual( + serverScid + ); + expect(new QUICConnectionId(serverHeaderRetry.dcid)).toEqual( + clientScid + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + const timeoutAfterRecv = clientConn.timeout(); + // The timeout is only reset after `recv` is called + expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); + expect( + new QUICConnectionId(clientHeaderInitialRetry.scid) + ).toEqual(clientScid); + // The DCID is now updated to the server generated one + expect( + new QUICConnectionId(clientHeaderInitialRetry.dcid) + ).toEqual(serverScid); + // The retried initial packet has the signed token + expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); + expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); + expect(clientHeaderInitialRetry.versions).toBeNull(); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + // Now that both the client and server has selected their own SCID, where + // the server derived its SCID from the initial client's randomly + // generated DCID, we can update their respective DCID + // This means that the client's connection ID is still the randomly + // generated SCID at the beginning, while the server's connection ID + // is the derived SCID when it sent the retry packet. + clientDcid = serverScid; + serverDcid = clientScid; + // Server receives the retried initial frame + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // The timeout is still null upon the first recv for the server + // This is only true because timeout is `0` which is `Infinity` + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Server's responds with an initial frame + expect(serverSendLength).toBe(1200); + // The server is now setting its timeout to start at 1 second + expect(serverConn.timeout()).toBeCloseTo(1000, -3); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + // At this point the server connection is still not established + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + const serverHeaderInitial = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual(serverScid); + expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual(serverDcid); + expect(serverHeaderInitial.token).toHaveLength(0); + expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); + expect(serverHeaderInitial.versions).toBeNull(); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + // Timeout is lowered + expect(clientConn.timeout()).toBeLessThan(100); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(new QUICConnectionId(serverHeaderHandshake.scid)).toEqual(serverScid); + expect(new QUICConnectionId(serverHeaderHandshake.dcid)).toEqual(serverDcid); + // Timeout is lowered + expect(serverConn.timeout()).toBeLessThan(100); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + // Client receives server's handshake frame + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderHandshake = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + // SCID is dropped on the short frame + expect(serverHeaderShort.scid).toHaveLength(0); + expect(new QUICConnectionId(serverHeaderShort.dcid)).toEqual( + clientScid + ); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + // Client connection timeout is now null + // Both client and server is established + // This is due to max idle timeout of 0 + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + // Timeout is lowered + expect(serverConn.timeout()).toBeLessThan(100); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + // SCID is dropped on the short frame + expect(clientHeaderShort.scid).toHaveLength(0); + expect(new QUICConnectionId(clientHeaderShort.dcid)).toEqual( + serverScid + ); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('Application Close')); + expect(clientConn.localError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(Buffer.from('Application Close')) + }); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + // The timeout begins again + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + // Connection is still established + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Connection however begins draining + expect(clientConn.isDraining()).toBeTrue(); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Client connection now waits to be closed + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + // Timeout is finally null + expect(clientConn.timeout()).toBeNull(); + // Connection did not timeout from idleness + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + // Connection is left as established + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Connection is fully closed + expect(clientConn.isClosed()).toBeTrue(); + // Connection is left as draining + expect(clientConn.isDraining()).toBeTrue(); + // -short-> SERVER + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // The server receives the client's error + expect(serverConn.peerError()).toEqual({ + isApp: true, + errorCode: 0, + reason: new Uint8Array(Buffer.from('Application Close')) + }); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // SERVER draining + expect(serverConn.isDraining()).toBeTrue(); + // Once the server is in draining, it does not need to respond + // it just waits to timeout, during that time, it is in "draining" state + // We need to exhaust the server's timeout to be fully closed + // Unlike TCP, there is no half-closed state for QUIC connections + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + // Connection did not timeout from idleness + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + // Connection is left as established + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + // Connection is fully closed + expect(serverConn.isClosed()).toBeTrue(); + // Connection is left as draining + expect(serverConn.isDraining()).toBeTrue(); + }); + }); + describe('connection between client and server with ECDSA', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client dialing', async () => { + // Send the initial packet + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + // The initial frame will always be 1200 bytes + expect(clientSendLength).toBe(1200); + expect(clientSendInfo.from).toEqual(clientHost); + expect(clientSendInfo.to).toEqual(serverHost); + // This is the initial delay for the dialing procedure + // Quiche will repeatedly send the initial packet until it is received + // or exhausted the idle timeout, which in this case is 0 (disabled) + expect(typeof clientConn.timeout()!).toBe('number'); + // The initial delay starts at roughly 1 second + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(1000, -3); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + // Repeating send will throw `Done` + // This proves that only 1 send is necessary at the beginning + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Wait out the delay (add 50ms for non-determinism) + await testsUtils.sleep(clientConn.timeout()! + 50); + // Connection has not timed out because idle timeout defaults to infinity + expect(clientConn.isTimedOut()).toBeFalse(); + // The delay is exhausted, and therefore should be 0 + expect(clientConn.timeout()).toBe(0); + // The `onTimeout` must be called to transition state + clientConn.onTimeout(); + // The delay is repeated immediately after `onTimeout` + // It is still 1 second + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(1000, -3); + // Retry the initial packet + const clientBuffer_ = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer_); + expect(clientSendLength).toBe(1200); + expect(clientSendInfo.from).toEqual(clientHost); + expect(clientSendInfo.to).toEqual(serverHost); + // Retried initial frame is not an exact copy + expect(clientBuffer_).not.toEqual(clientBuffer); + // Upon the retry, the delay now doubles + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(2000, -3); + // This dialing process will repeat max idle timeout is exhausted + // Copy sendBuffer_ into sendBuffer + clientBuffer.set(clientBuffer_); + }); + test('client and server negotiation', async () => { + // Process the initial frame + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // It will be an initial packet + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + // The SCID is what was generated above + expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual(clientScid); + // The DCID is randomly generated by the client + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + expect(clientDcid).not.toEqual(clientScid); + // The token will be empty + expect(clientHeaderInitial.token).toHaveLength(0); + // The version should be 1 + expect(clientHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); + expect(clientHeaderInitial.versions).toBeNull(); + // Version negotiation + // The version is supported, we don't need to change + expect(quiche.versionIsSupported(clientHeaderInitial.version)).toBeTrue(); + // Derives a new SCID by signing the client's generated DCID + // This is only used during the stateless retry + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + const timeoutBeforeRecv = clientConn.timeout(); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + const timeoutAfterRecv = clientConn.timeout(); + // The timeout is only reset after `recv` is called + expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); + expect( + new QUICConnectionId(clientHeaderInitialRetry.scid) + ).toEqual(clientScid); + // The DCID is now updated to the server generated one + expect( + new QUICConnectionId(clientHeaderInitialRetry.dcid) + ).toEqual(serverScid); + // The retried initial packet has the signed token + expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); + expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); + expect(clientHeaderInitialRetry.versions).toBeNull(); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + // Now that both the client and server has selected their own SCID, where + // the server derived its SCID from the initial client's randomly + // generated DCID, we can update their respective DCID + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // The timeout is still null upon the first recv for the server + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Server's responds with an initial frame + expect(serverSendLength).toBe(1200); + // The server is now setting its timeout to start at 1 second + expect(serverConn.timeout()).toBeCloseTo(1000, -3); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + // At this point the server connection is still not established + const serverHeaderInitial = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual(serverScid); + expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual(serverDcid); + expect(serverHeaderInitial.token).toHaveLength(0); + expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); + expect(serverHeaderInitial.versions).toBeNull(); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + // Timeout is lowered + expect(clientConn.timeout()).toBeLessThan(100); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + // Client connection timeout is now null + // Both client and server is established + // This is due to max idle timeout of 0 + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + // Timeout is lowered + expect(serverConn.timeout()).toBeLessThan(100); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(false, 2, new Uint8Array()); + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: 2, + reason: new Uint8Array() + }); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + // The timeout begins again + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + // Connection is still established + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Connection however begins draining + expect(clientConn.isDraining()).toBeTrue(); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Client connection now waits to be closed + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + // Timeout is finally null + expect(clientConn.timeout()).toBeNull(); + // Connection did not timeout from idleness + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + // Connection is left as established + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Connection is fully closed + expect(clientConn.isClosed()).toBeTrue(); + // Connection is left as draining + expect(clientConn.isDraining()).toBeTrue(); + // -short-> SERVER + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: 2, + reason: new Uint8Array() + }); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // SERVER draining + expect(serverConn.isDraining()).toBeTrue(); + // Once the server is in draining, it does not need to respond + // it just waits to timeout, during that time, it is in "draining" state + // We need to exhaust the server's timeout to be fully closed + // Unlike TCP, there is no half-closed state for QUIC connections + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + // Connection did not timeout from idleness + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + // Connection is left as established + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + // Connection is fully closed + expect(serverConn.isClosed()).toBeTrue(); + // Connection is left as draining + expect(serverConn.isDraining()).toBeTrue(); + }); + }); + describe('connection between client and server with Ed25519', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: false, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client dialing', async () => { + // Send the initial packet + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + // The initial frame will always be 1200 bytes + expect(clientSendLength).toBe(1200); + expect(clientSendInfo.from).toEqual(clientHost); + expect(clientSendInfo.to).toEqual(serverHost); + // This is the initial delay for the dialing procedure + // Quiche will repeatedly send the initial packet until it is received + // or exhausted the idle timeout, which in this case is 0 (disabled) + expect(typeof clientConn.timeout()!).toBe('number'); + // The initial delay starts at roughly 1 second + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(1000, -3); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + // Repeating send will throw `Done` + // This proves that only 1 send is necessary at the beginning + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Wait out the delay (add 50ms for non-determinism) + await testsUtils.sleep(clientConn.timeout()! + 50); + // Connection has not timed out because idle timeout defaults to infinity + expect(clientConn.isTimedOut()).toBeFalse(); + // The delay is exhausted, and therefore should be 0 + expect(clientConn.timeout()).toBe(0); + // The `onTimeout` must be called to transition state + clientConn.onTimeout(); + // The delay is repeated immediately after `onTimeout` + // It is still 1 second + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(1000, -3); + // Retry the initial packet + const clientBuffer_ = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer_); + expect(clientSendLength).toBe(1200); + expect(clientSendInfo.from).toEqual(clientHost); + expect(clientSendInfo.to).toEqual(serverHost); + // Retried initial frame is not an exact copy + expect(clientBuffer_).not.toEqual(clientBuffer); + // Upon the retry, the delay now doubles + // Round to the nearest 1000 + expect(clientConn.timeout()).toBeCloseTo(2000, -3); + // This dialing process will repeat max idle timeout is exhausted + // Copy sendBuffer_ into sendBuffer + clientBuffer.set(clientBuffer_); + }); + test('client and server negotiation', async () => { + // Process the initial frame + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // It will be an initial packet + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + // The SCID is what was generated above + expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual(clientScid); + // The DCID is randomly generated by the client + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + expect(clientDcid).not.toEqual(clientScid); + // The token will be empty + expect(clientHeaderInitial.token).toHaveLength(0); + // The version should be 1 + expect(clientHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); + expect(clientHeaderInitial.versions).toBeNull(); + // Version negotiation + // The version is supported, we don't need to change + expect(quiche.versionIsSupported(clientHeaderInitial.version)).toBeTrue(); + // Derives a new SCID by signing the client's generated DCID + // This is only used during the stateless retry + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + const timeoutBeforeRecv = clientConn.timeout(); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + const timeoutAfterRecv = clientConn.timeout(); + // The timeout is only reset after `recv` is called + expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); + expect( + new QUICConnectionId(clientHeaderInitialRetry.scid) + ).toEqual(clientScid); + // The DCID is now updated to the server generated one + expect( + new QUICConnectionId(clientHeaderInitialRetry.dcid) + ).toEqual(serverScid); + // The retried initial packet has the signed token + expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); + expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); + expect(clientHeaderInitialRetry.versions).toBeNull(); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + // Now that both the client and server has selected their own SCID, where + // the server derived its SCID from the initial client's randomly + // generated DCID, we can update their respective DCID + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + // The timeout is still null upon the first recv for the server + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Server's responds with an initial frame + expect(serverSendLength).toBe(1200); + // The server is now setting its timeout to start at 1 second + expect(serverConn.timeout()).toBeCloseTo(1000, -3); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + // At this point the server connection is still not established + const serverHeaderInitial = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual(serverScid); + expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual(serverDcid); + expect(serverHeaderInitial.token).toHaveLength(0); + expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); + expect(serverHeaderInitial.versions).toBeNull(); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + // Timeout is lowered + expect(clientConn.timeout()).toBeLessThan(100); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + // Client connection timeout is now null + // Both client and server is established + // This is due to max idle timeout of 0 + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + // Timeout is lowered + expect(serverConn.timeout()).toBeLessThan(100); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(false, 1, Buffer.from('')); + expect(clientConn.localError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array() + }); + expect(clientConn.timeout()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + // The timeout begins again + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + // Connection is still established + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Connection however begins draining + expect(clientConn.isDraining()).toBeTrue(); + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + // Client connection now waits to be closed + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + // Timeout is finally null + expect(clientConn.timeout()).toBeNull(); + // Connection did not timeout from idleness + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + // Connection is left as established + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + // Connection is fully closed + expect(clientConn.isClosed()).toBeTrue(); + // Connection is left as draining + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(serverConn.peerError()).toEqual({ + isApp: false, + errorCode: 1, + reason: new Uint8Array() + }); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // SERVER draining + expect(serverConn.isDraining()).toBeTrue(); + // Once the server is in draining, it does not need to respond + // it just waits to timeout, during that time, it is in "draining" state + // We need to exhaust the server's timeout to be fully closed + // Unlike TCP, there is no half-closed state for QUIC connections + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + // Connection did not timeout from idleness + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + // Connection is left as established + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + // Connection is fully closed + expect(serverConn.isClosed()).toBeTrue(); + // Connection is left as draining + expect(serverConn.isDraining()).toBeTrue(); + }); + }); + }); +}); diff --git a/tests/native/quiche.test.ts b/tests/native/quiche.test.ts new file mode 100644 index 00000000..ce7ec2de --- /dev/null +++ b/tests/native/quiche.test.ts @@ -0,0 +1,36 @@ +import { quiche } from '@/native'; +import * as testsUtils from '../utils'; + +describe('quiche', () => { + test('frame parsing', async () => { + let frame: Buffer; + frame = Buffer.from('hello world'); + expect(() => quiche.Header.fromSlice( + frame, + quiche.MAX_CONN_ID_LEN) + ).toThrow( + 'BufferTooShort' + ); + // `InvalidPacket` is also possible but even random bytes can + // look like a packet, so it's not tested here + }); + test('version negotiation', async () => { + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await testsUtils.randomBytes(scidBuffer); + const scid = new Uint8Array(scidBuffer); + const dcidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await testsUtils.randomBytes(dcidBuffer); + const dcid = new Uint8Array(dcidBuffer); + const versionPacket = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const versionPacketLength = quiche.negotiateVersion( + scid, + dcid, + versionPacket + ); + const serverHeaderVersion = quiche.Header.fromSlice( + versionPacket.subarray(0, versionPacketLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderVersion.ty).toBe(quiche.Type.VersionNegotiation); + }); +}); diff --git a/tests/native/quiche.tls.test.ts b/tests/native/quiche.tls.test.ts new file mode 100644 index 00000000..ab28047f --- /dev/null +++ b/tests/native/quiche.tls.test.ts @@ -0,0 +1,2019 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { QUICConfig, Crypto, Host, Hostname, Port } from '@/types'; +import type { Config, Connection, SendInfo } from '@/native/types'; +import { quiche } from '@/native'; +import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; +import QUICConnectionId from '@/QUICConnectionId'; +import * as utils from '@/utils'; +import * as testsUtils from '../utils'; + +describe('quiche tls', () => { + let crypto: { + key: ArrayBuffer; + ops: Crypto; + }; + let keyPairRSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certRSA: X509Certificate; + let keyPairRSAPEM: { + publicKey: string; + privateKey: string; + }; + let certRSAPEM: string; + let keyPairECDSA: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certECDSA: X509Certificate; + let keyPairECDSAPEM: { + publicKey: string; + privateKey: string; + }; + let certECDSAPEM: string; + let keyPairEd25519: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + let certEd25519: X509Certificate; + let keyPairEd25519PEM: { + publicKey: string; + privateKey: string; + }; + let certEd25519PEM: string; + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + keyPairRSA = await testsUtils.generateKeyPairRSA(); + certRSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairRSA, + issuerPrivateKey: keyPairRSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairRSAPEM = await testsUtils.keyPairRSAToPEM(keyPairRSA); + certRSAPEM = testsUtils.certToPEM(certRSA); + keyPairECDSA = await testsUtils.generateKeyPairECDSA(); + certECDSA = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairECDSA, + issuerPrivateKey: keyPairECDSA.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairECDSAPEM = await testsUtils.keyPairECDSAToPEM(keyPairECDSA); + certECDSAPEM = testsUtils.certToPEM(certECDSA); + keyPairEd25519 = await testsUtils.generateKeyPairEd25519(); + certEd25519 = await testsUtils.generateCertificate({ + certId: '0', + subjectKeyPair: keyPairEd25519, + issuerPrivateKey: keyPairEd25519.privateKey, + duration: 60 * 60 * 24 * 365 * 10, + }); + keyPairEd25519PEM = await testsUtils.keyPairEd25519ToPEM(keyPairEd25519); + certEd25519PEM = testsUtils.certToPEM(certEd25519); + }); + describe('RSA success', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -handshake-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + // Server rejects client handshake + expect( + () => + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ) + ).toThrow('TlsFail'); + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('RSA fail verifying server', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Client rejects server handshake + expect(() => + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ) + ).toThrow('TlsFail'); + + expect(clientConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(clientConn.peerError()).toBeNull(); + + + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client -handshake-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderHandshake = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA success', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + expect(() => + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ) + ).toThrow('TlsFail'); + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA fail verifying server', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Client rejects server initial + expect(() => + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ) + ).toThrow('TlsFail'); + expect(clientConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 success', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + expect(() => + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ) + ).toThrow('TlsFail'); + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying server', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign( + crypto.key, + clientDcid, + ), + 0, + quiche.MAX_CONN_ID_LEN + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram + ); + // Retry gets sent back to be processed by the client + clientConn.recv( + retryDatagram.subarray(0, retryDatagramLength), + { + to: clientHost, + from: serverHost + } + ); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Client rejects server initial + expect(() => + clientConn.recv( + serverBuffer.subarray(0, serverSendLength), + { + to: clientHost, + from: serverHost + } + ) + ).toThrow('TlsFail'); + + + expect(clientConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + expect(clientConn.peerError()).toBeNull(); + + + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv( + clientBuffer.subarray(0, clientSendLength), + { + to: serverHost, + from: clientHost + } + ); + + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array() + }); + + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 2567b74f..f429800b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,20 +1,486 @@ +import type { X509Certificate } from '@peculiar/x509'; +import type { Connection } from '@/native/types'; import type QUICSocket from '@/QUICSocket'; import type QUICClient from '@/QUICClient'; import type QUICServer from '@/QUICServer'; import type QUICStream from '@/QUICStream'; -import { webcrypto } from 'crypto'; +import { Crypto } from '@peculiar/webcrypto'; +import * as x509 from '@peculiar/x509'; +import { fc } from '@fast-check/jest'; + +/** + * WebCrypto polyfill from @peculiar/webcrypto + * This behaves differently with respect to Ed25519 keys + * See: https://github.com/PeculiarVentures/webcrypto/issues/55 + */ +const webcrypto = new Crypto(); + +/** + * Monkey patches the global crypto object polyfill + */ +globalThis.crypto = webcrypto; + +x509.cryptoProvider.set(webcrypto); async function sleep(ms: number): Promise { return await new Promise((r) => setTimeout(r, ms)); } +async function randomBytes(data: ArrayBuffer) { + webcrypto.getRandomValues(new Uint8Array(data)); +} + +/** + * Generates RSA keypair + */ +async function generateKeyPairRSA(): Promise<{ + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}> { + const keyPair = await webcrypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + return { + publicKey: await webcrypto.subtle.exportKey('jwk', keyPair.publicKey), + privateKey: await webcrypto.subtle.exportKey('jwk', keyPair.privateKey), + }; +} + +/** + * Generates ECDSA keypair + */ +async function generateKeyPairECDSA(): Promise<{ + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}> { + const keyPair = await webcrypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + true, + ['sign', 'verify'], + ); + return { + publicKey: await webcrypto.subtle.exportKey('jwk', keyPair.publicKey), + privateKey: await webcrypto.subtle.exportKey('jwk', keyPair.privateKey), + }; +} + +/** + * Generates Ed25519 keypair + * This uses `@peculiar/webcrypto` API + */ +async function generateKeyPairEd25519(): Promise<{ + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}> { + const keyPair = (await webcrypto.subtle.generateKey( + { + name: 'EdDSA', + namedCurve: 'Ed25519', + }, + true, + ['sign', 'verify'], + )) as CryptoKeyPair; + return { + publicKey: await webcrypto.subtle.exportKey('jwk', keyPair.publicKey), + privateKey: await webcrypto.subtle.exportKey('jwk', keyPair.privateKey), + }; +} + +/** + * Imports public key. + * This uses `@peculiar/webcrypto` API for Ed25519 keys. + */ +async function importPublicKey(publicKey: JsonWebKey): Promise { + let algorithm; + switch (publicKey.kty) { + case 'RSA': + switch (publicKey.alg) { + case 'RS256': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + }; + break; + case 'RS384': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-384', + }; + break; + case 'RS512': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-512', + }; + break; + default: + throw new Error(`Unsupported algorithm ${publicKey.alg}`); + } + break; + case 'EC': + switch (publicKey.crv) { + case 'P-256': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-256', + }; + break; + case 'P-384': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-384', + }; + break; + case 'P-521': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-521', + }; + break; + default: + throw new Error(`Unsupported curve ${publicKey.crv}`); + } + break; + case 'OKP': + algorithm = { + name: 'EdDSA', + namedCurve: 'Ed25519', + }; + break; + default: + throw new Error(`Unsupported key type ${publicKey.kty}`); + } + return await webcrypto.subtle.importKey('jwk', publicKey, algorithm, true, [ + 'verify', + ]); +} + +/** + * Imports private key. + * This uses `@peculiar/webcrypto` API for Ed25519 keys. + */ +async function importPrivateKey(privateKey: JsonWebKey): Promise { + let algorithm; + switch (privateKey.kty) { + case 'RSA': + switch (privateKey.alg) { + case 'RS256': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + }; + break; + case 'RS384': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-384', + }; + break; + case 'RS512': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-512', + }; + break; + default: + throw new Error(`Unsupported algorithm ${privateKey.alg}`); + } + break; + case 'EC': + switch (privateKey.crv) { + case 'P-256': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-256', + }; + break; + case 'P-384': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-384', + }; + break; + case 'P-521': + algorithm = { + name: 'ECDSA', + namedCurve: 'P-521', + }; + break; + default: + throw new Error(`Unsupported curve ${privateKey.crv}`); + } + break; + case 'OKP': + algorithm = { + name: 'EdDSA', + namedCurve: 'Ed25519', + }; + break; + default: + throw new Error(`Unsupported key type ${privateKey.kty}`); + } + return await webcrypto.subtle.importKey('jwk', privateKey, algorithm, true, [ + 'sign', + ]); +} + +async function keyPairRSAToPEM(keyPair: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}): Promise<{ + publicKey: string; + privateKey: string; +}> { + const publicKey = await importPublicKey(keyPair.publicKey); + const privatekey = await importPrivateKey(keyPair.privateKey); + const publicKeySPKI = await webcrypto.subtle.exportKey('spki', publicKey); + const publicKeySPKIBuffer = Buffer.from(publicKeySPKI); + const publicKeyPEMBody = + publicKeySPKIBuffer + .toString('base64') + .replace(/(.{64})/g, '$1\n') + .trimEnd() + '\n'; + const publicKeyPEM = `-----BEGIN PUBLIC KEY-----\n${publicKeyPEMBody}\n-----END PUBLIC KEY-----\n`; + const privateKeyPKCS8 = await webcrypto.subtle.exportKey('pkcs8', privatekey); + const privateKeyPKCS8Buffer = Buffer.from(privateKeyPKCS8); + const privateKeyPEMBody = + privateKeyPKCS8Buffer + .toString('base64') + .replace(/(.{64})/g, '$1\n') + .trimEnd() + '\n'; + const privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyPEMBody}-----END PRIVATE KEY-----\n`; + return { + publicKey: publicKeyPEM, + privateKey: privateKeyPEM, + }; +} + +async function keyPairECDSAToPEM(keyPair: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}): Promise<{ + publicKey: string; + privateKey: string; +}> { + const publicKey = await importPublicKey(keyPair.publicKey); + const privatekey = await importPrivateKey(keyPair.privateKey); + const publicKeySPKI = await webcrypto.subtle.exportKey('spki', publicKey); + const publicKeySPKIBuffer = Buffer.from(publicKeySPKI); + const publicKeyPEMBody = + publicKeySPKIBuffer + .toString('base64') + .replace(/(.{64})/g, '$1\n') + .trimEnd() + '\n'; + const publicKeyPEM = `-----BEGIN PUBLIC KEY-----\n${publicKeyPEMBody}\n-----END PUBLIC KEY-----\n`; + const privateKeyPKCS8 = await webcrypto.subtle.exportKey('pkcs8', privatekey); + const privateKeyPKCS8Buffer = Buffer.from(privateKeyPKCS8); + const privateKeyPEMBody = + privateKeyPKCS8Buffer + .toString('base64') + .replace(/(.{64})/g, '$1\n') + .trimEnd() + '\n'; + const privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyPEMBody}-----END PRIVATE KEY-----\n`; + return { + publicKey: publicKeyPEM, + privateKey: privateKeyPEM, + }; +} + +async function keyPairEd25519ToPEM(keyPair: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; +}): Promise<{ + publicKey: string; + privateKey: string; +}> { + const publicKey = await importPublicKey(keyPair.publicKey); + const privatekey = await importPrivateKey(keyPair.privateKey); + const publicKeySPKI = await webcrypto.subtle.exportKey('spki', publicKey); + const publicKeySPKIBuffer = Buffer.from(publicKeySPKI); + const publicKeyPEMBody = + publicKeySPKIBuffer + .toString('base64') + .replace(/(.{64})/g, '$1\n') + .trimEnd() + '\n'; + const publicKeyPEM = `-----BEGIN PUBLIC KEY-----\n${publicKeyPEMBody}\n-----END PUBLIC KEY-----\n`; + const privateKeyPKCS8 = await webcrypto.subtle.exportKey('pkcs8', privatekey); + const privateKeyPKCS8Buffer = Buffer.from(privateKeyPKCS8); + const privateKeyPEMBody = + privateKeyPKCS8Buffer + .toString('base64') + .replace(/(.{64})/g, '$1\n') + .trimEnd() + '\n'; + const privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyPEMBody}-----END PRIVATE KEY-----\n`; + return { + publicKey: publicKeyPEM, + privateKey: privateKeyPEM, + }; +} + +const extendedKeyUsageFlags = { + serverAuth: '1.3.6.1.5.5.7.3.1', + clientAuth: '1.3.6.1.5.5.7.3.2', + codeSigning: '1.3.6.1.5.5.7.3.3', + emailProtection: '1.3.6.1.5.5.7.3.4', + timeStamping: '1.3.6.1.5.5.7.3.8', + ocspSigning: '1.3.6.1.5.5.7.3.9', +}; + +/** + * Generate x509 certificate. + * Duration is in seconds. + * X509 certificates currently use `UTCTime` format for `notBefore` and `notAfter`. + * This means: + * - Only second resolution. + * - Minimum date for validity is 1970-01-01T00:00:00Z (inclusive). + * - Maximum date for valdity is 2049-12-31T23:59:59Z (inclusive). + */ +async function generateCertificate({ + certId, + subjectKeyPair, + issuerPrivateKey, + duration, + subjectAttrsExtra = [], + issuerAttrsExtra = [], + now = new Date(), +}: { + certId: string; + subjectKeyPair: { + publicKey: JsonWebKey; + privateKey: JsonWebKey; + }; + issuerPrivateKey: JsonWebKey; + duration: number; + subjectAttrsExtra?: Array<{ [key: string]: Array }>; + issuerAttrsExtra?: Array<{ [key: string]: Array }>; + now?: Date; +}): Promise { + const certIdNum = parseInt(certId); + const iss = certIdNum === 0 ? certIdNum : certIdNum - 1; + const sub = certIdNum; + const subjectPublicCryptoKey = await importPublicKey( + subjectKeyPair.publicKey, + ); + const subjectPrivateCryptoKey = await importPrivateKey( + subjectKeyPair.privateKey, + ); + const issuerPrivateCryptoKey = await importPrivateKey(issuerPrivateKey); + if (duration < 0) { + throw new RangeError('`duration` must be positive'); + } + // X509 `UTCTime` format only has resolution of seconds + // this truncates to second resolution + const notBeforeDate = new Date(now.getTime() - (now.getTime() % 1000)); + const notAfterDate = new Date(now.getTime() - (now.getTime() % 1000)); + // If the duration is 0, then only the `now` is valid + notAfterDate.setSeconds(notAfterDate.getSeconds() + duration); + if (notBeforeDate < new Date(0)) { + throw new RangeError( + '`notBeforeDate` cannot be before 1970-01-01T00:00:00Z', + ); + } + if (notAfterDate > new Date(new Date('2050').getTime() - 1)) { + throw new RangeError('`notAfterDate` cannot be after 2049-12-31T23:59:59Z'); + } + const serialNumber = certId; + // The entire subject attributes and issuer attributes + // is constructed via `x509.Name` class + // By default this supports on a limited set of names: + // CN, L, ST, O, OU, C, DC, E, G, I, SN, T + // If custom names are desired, this needs to change to constructing + // `new x509.Name('FOO=BAR', { FOO: '1.2.3.4' })` manually + // And each custom attribute requires a registered OID + // Because the OID is what is encoded into ASN.1 + const subjectAttrs = [ + { + CN: [`${sub}`], + }, + // Filter out conflicting CN attributes + ...subjectAttrsExtra.filter((attr) => !('CN' in attr)), + ]; + const issuerAttrs = [ + { + CN: [`${iss}`], + }, + // Filter out conflicting CN attributes + ...issuerAttrsExtra.filter((attr) => !('CN' in attr)), + ]; + const signingAlgorithm: any = issuerPrivateCryptoKey.algorithm; + if (signingAlgorithm.name === 'ECDSA') { + // In ECDSA, the signature should match the curve strength + switch (signingAlgorithm.namedCurve) { + case 'P-256': + signingAlgorithm.hash = 'SHA-256'; + break; + case 'P-384': + signingAlgorithm.hash = 'SHA-384'; + break; + case 'P-521': + signingAlgorithm.hash = 'SHA-512'; + break; + default: + throw new TypeError( + `Issuer private key has an unsupported curve: ${signingAlgorithm.namedCurve}`, + ); + } + } + const certConfig = { + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + subject: subjectAttrs, + issuer: issuerAttrs, + signingAlgorithm, + publicKey: subjectPublicCryptoKey, + signingKey: subjectPrivateCryptoKey, + extensions: [ + new x509.BasicConstraintsExtension(true, undefined, true), + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.nonRepudiation | + x509.KeyUsageFlags.keyAgreement | + x509.KeyUsageFlags.keyEncipherment | + x509.KeyUsageFlags.dataEncipherment, + true, + ), + new x509.ExtendedKeyUsageExtension([ + extendedKeyUsageFlags.serverAuth, + extendedKeyUsageFlags.clientAuth, + extendedKeyUsageFlags.codeSigning, + extendedKeyUsageFlags.emailProtection, + extendedKeyUsageFlags.timeStamping, + extendedKeyUsageFlags.ocspSigning, + ]), + await x509.SubjectKeyIdentifierExtension.create(subjectPublicCryptoKey), + ] as Array, + }; + certConfig.signingKey = issuerPrivateCryptoKey; + return await x509.X509CertificateGenerator.create(certConfig); +} + +function certToPEM(cert: X509Certificate): string { + return cert.toString('pem') + '\n'; +} + /** * Generate 256-bit HMAC key using webcrypto. * Web Crypto prefers using the `CryptoKey` type. * But to be fully generic, we use the `ArrayBuffer` type. * In production, prefer to use libsodium as it would be faster. */ -async function generateKey(): Promise { +async function generateKeyHMAC(): Promise { const cryptoKey = await webcrypto.subtle.generateKey( { name: 'HMAC', @@ -33,7 +499,7 @@ async function generateKey(): Promise { * But to be fully generic, we use the `ArrayBuffer` type. * In production, prefer to use libsodium as it would be faster. */ -async function sign(key: ArrayBuffer, data: ArrayBuffer) { +async function signHMAC(key: ArrayBuffer, data: ArrayBuffer) { const cryptoKey = await webcrypto.subtle.importKey( 'raw', key, @@ -53,7 +519,11 @@ async function sign(key: ArrayBuffer, data: ArrayBuffer) { * But to be fully generic, we use the `ArrayBuffer` type. * In production, prefer to use libsodium as it would be faster. */ -async function verify(key: ArrayBuffer, data: ArrayBuffer, sig: ArrayBuffer) { +async function verifyHMAC( + key: ArrayBuffer, + data: ArrayBuffer, + sig: ArrayBuffer, +) { const cryptoKey = await webcrypto.subtle.importKey( 'raw', key, @@ -67,10 +537,6 @@ async function verify(key: ArrayBuffer, data: ArrayBuffer, sig: ArrayBuffer) { return webcrypto.subtle.verify('HMAC', cryptoKey, sig, data); } -async function randomBytes(data: ArrayBuffer) { - webcrypto.getRandomValues(new Uint8Array(data)); -} - /** * Use this on every client or server. It is essential for cleaning them up. */ @@ -135,13 +601,41 @@ const handleStreamProm = async (stream: QUICStream, streamData: StreamData) => { } }; -export type { Messages, StreamData }; +/** + * When the `conn.timeout()` returns `0`, it is still a valid timeout. + * Only when it returns `null`, is the timeout fully exhausted. + * This should only be called after `conn.onTimeout()` is triggered. + * This is useful for tests that need to exhaust the timeout. + */ +async function waitForTimeoutNull(conn: Connection): Promise { + while (true) { + const timeout = conn.timeout(); + if (timeout === 0) { + await sleep(timeout); + conn.onTimeout(); + } else if (timeout == null) { + return; + } + } +} + export { sleep, - generateKey, - sign, - verify, randomBytes, + generateKeyPairRSA, + generateKeyPairECDSA, + generateKeyPairEd25519, + keyPairRSAToPEM, + keyPairECDSAToPEM, + keyPairEd25519ToPEM, + generateCertificate, + certToPEM, + generateKeyHMAC, + signHMAC, + verifyHMAC, extractSocket, handleStreamProm, + waitForTimeoutNull, }; + +export type { Messages, StreamData }; diff --git a/tsconfig.json b/tsconfig.json index 2fffd283..a1204365 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "resolveJsonModule": true, "moduleResolution": "node", "module": "CommonJS", - "target": "ES2021", + "target": "ES2022", "baseUrl": "./src", "paths": { "@": ["index"], From 498f7aa976414fc2cd4e86e2ef2432b152345fcd Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Tue, 20 Jun 2023 17:08:33 +1000 Subject: [PATCH 02/22] feat: applying monitor locking to serialize async events [ci skip] --- package-lock.json | 38 ++++++++++++++++++++++---------- package.json | 2 +- src/QUICConnection.ts | 51 +++++++++++++++---------------------------- src/QUICStream.ts | 6 ++--- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index a03323b8..21463b70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@matrixai/async-cancellable": "^1.1.0", "@matrixai/async-init": "^1.8.3", - "@matrixai/async-locks": "^3.2.0", + "@matrixai/async-locks": "^4.0.0", "@matrixai/contexts": "^1.0.0", "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^3.1.0", @@ -1179,9 +1179,9 @@ } }, "node_modules/@matrixai/async-cancellable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@matrixai/async-cancellable/-/async-cancellable-1.1.0.tgz", - "integrity": "sha512-DxhRAOobxD+bolR93g0jXbpt7X0AeY8ELcT1TgpoQm8jH8KfJxMxguzijozd9Jm1HKcexshiDeBa/COt8p6eJA==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@matrixai/async-cancellable/-/async-cancellable-1.1.1.tgz", + "integrity": "sha512-f0yxu7dHwvffZ++7aCm2WIcCJn18uLcOTdCCwEA3R3KVHYE3TG/JNoTWD9/mqBkAV1AI5vBfJzg27WnF9rOUXQ==" }, "node_modules/@matrixai/async-init": { "version": "1.8.3", @@ -1192,7 +1192,7 @@ "@matrixai/errors": "^1.1.7" } }, - "node_modules/@matrixai/async-locks": { + "node_modules/@matrixai/async-init/node_modules/@matrixai/async-locks": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-3.2.0.tgz", "integrity": "sha512-Gl919y3GK2lBCI7M3MabE2u0+XOhKqqgwFEGVaPSI2BrdSI+RY7K3+dzjTSUTujVZwiYskT611CBvlDm9fhsNg==", @@ -1202,6 +1202,17 @@ "async-mutex": "^0.3.2" } }, + "node_modules/@matrixai/async-locks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-4.0.0.tgz", + "integrity": "sha512-u/3fOdtjOKcDYF8dDoPR1/+7nmOkhxo42eBpXTEgfI0hLPGI37PoW7tjLvwy+O51Quy1HGOwhsR/Dgr4x+euug==", + "dependencies": { + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/errors": "^1.1.7", + "@matrixai/resources": "^1.1.5", + "@matrixai/timer": "^1.1.1" + } + }, "node_modules/@matrixai/contexts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@matrixai/contexts/-/contexts-1.0.0.tgz", @@ -1261,15 +1272,17 @@ ] }, "node_modules/@matrixai/resources": { - "version": "1.1.4", - "license": "Apache-2.0" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@matrixai/resources/-/resources-1.1.5.tgz", + "integrity": "sha512-m/DEZEe3wHqWEPTyoBtzFF6U9vWYhEnQtGgwvqiAlTxTM0rk96UBpWjDZCTF/vYG11ZlmlQFtg5H+zGgbjaB3Q==" }, "node_modules/@matrixai/timer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@matrixai/timer/-/timer-1.1.0.tgz", - "integrity": "sha512-n9ulMGJhjQyu+fhRxvP5SwY57miOLbRE6gtIzTZD6x7wFL0k5sUBMnEQ6W4QkO1atZnxKMFigg4gt/0V4XlniA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@matrixai/timer/-/timer-1.1.1.tgz", + "integrity": "sha512-8UKDoGuwKC6BvrY/yANJVH29v71wgQKH/tJlxMPohGxmzVUQO5+JeI4lUYVHTs2vq1AyKAWloF5fOig+I1dyGA==", "dependencies": { - "@matrixai/async-cancellable": "^1.0.4" + "@matrixai/async-cancellable": "^1.1.1", + "@matrixai/errors": "^1.1.7" } }, "node_modules/@napi-rs/cli": { @@ -2213,7 +2226,8 @@ }, "node_modules/async-mutex": { "version": "0.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", "dependencies": { "tslib": "^2.3.1" } diff --git a/package.json b/package.json index bce1496c..b2f60004 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@matrixai/async-cancellable": "^1.1.0", "@matrixai/contexts": "^1.0.0", "@matrixai/async-init": "^1.8.3", - "@matrixai/async-locks": "^3.2.0", + "@matrixai/async-locks": "^4.0.0", "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^3.1.0", "@matrixai/resources": "^1.1.3", diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 6d05926f..fda514d3 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -9,7 +9,7 @@ import type { StreamCodeToReason, StreamReasonToCode } from './types'; import type { QUICConfig, ConnectionMetadata } from './types'; import { StartStop, ready, status, running } from '@matrixai/async-init/dist/StartStop'; import Logger from '@matrixai/logger'; -import { Lock, LockBox } from '@matrixai/async-locks'; +import { Lock, LockBox, Monitor, RWLockWriter } from '@matrixai/async-locks'; import { destroyed } from '@matrixai/async-init'; import { Timer } from '@matrixai/timer'; import { buildQuicheConfig } from './config'; @@ -51,14 +51,6 @@ class QUICConnection extends EventTarget { */ public readonly conn: Connection; - /** - * Internal conn state transition lock. - * This is used to serialize state transitions to `conn`. - * This is also used by `QUICSocket`. - * @internal - */ - public readonly connLock: Lock = new Lock(); - /** * Internal stream map. * This is also used by `QUICStream`. @@ -183,7 +175,6 @@ class QUICConnection extends EventTarget { */ protected closedP: Promise; - protected wasEstablished: boolean = false; protected resolveEstablishedP: () => void; @@ -195,6 +186,9 @@ class QUICConnection extends EventTarget { protected lastErrorMessage?: string; + protected lockbox = new LockBox(); + protected readonly lockCode = 'Lock'; + public constructor({ type, scid, @@ -398,27 +392,19 @@ class QUICConnection extends EventTarget { errorMessage?: string; force?: boolean; }= {}, - lock: Lock = this.connLock + mon: Monitor, ) { this.logger.info(`Stop ${this.constructor.name}`); + await mon.withF(this.lockCode, async () => { + const streamDestroyPs: Array> = []; for (const stream of this.streamMap.values()) { - // TODO: ensure that `stream.destroy` understands `force` - // Without it, it should be graceful - // With it, then it should assume the rest of the system could be broken streamDestroyPs.push(stream.destroy({ force })); } await Promise.all(streamDestroyPs); // Do we do this afterwards or before? this.stopKeepAliveIntervalTimer(); try { - // We need to lock the connLock - // Note that this has no timeout - // We must have any deadlocks here! - // Plus ctx isn't accepted by the async-locks yet - // If already closed this will error out - // But nothing will happen - await this.connLock.withF(async () => { // If this is already closed, then `Done` will be thrown // Otherwise it can send `CONNECTION_CLOSE` frame // This can be 0x1c close at the QUIC layer or no errors @@ -431,7 +417,6 @@ class QUICConnection extends EventTarget { // If we get a `Done` exception we don't bother calling send // The send only gets sent if the `Done` is not the case await this.send(); - }); } catch (e) { // If the connection is already closed, `Done` will be thrown if (e.message !== 'Done') { @@ -450,6 +435,7 @@ class QUICConnection extends EventTarget { this.socket.connectionMap.delete(this.connectionId); this.dispatchEvent(new events.QUICConnectionStopEvent()); + }); this.logger.info(`Stopped ${this.constructor.name}`); } @@ -656,9 +642,8 @@ class QUICConnection extends EventTarget { * Any errors must be emitted as events. * @internal */ - public async send(lock: Lock = this.connLock): Promise { - - await this.connLock.withF(async () => { + public async send(mon: Monitor): Promise { + await mon.withF(this.lockCode, async () => { const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE); let sendLength: number; let sendInfo: SendInfo; @@ -742,7 +727,7 @@ class QUICConnection extends EventTarget { // Note that this may cause the client to attempt // to stop the socket and stuff // The client should ignore this event error - // Becuase it's actually being handled + // Because it's actually being handled // On the other hand... // If we randomly fail here // It's correct to properly raise an event @@ -758,7 +743,7 @@ class QUICConnection extends EventTarget { // At the same time, we may in fact be closed too if (this.conn.isClosed()) { // We actually finally closed here - // Actually theq uestion is that this could be an error + // Actually the question is that this could be an error // The act of closing is an error? // That's confusing this.resolveClosedP(); @@ -925,7 +910,7 @@ class QUICConnection extends EventTarget { // }, intervalDelay); // } // } - + // // // Timeout handling, these methods handle time keeping for quiche. // // Quiche will request an amount of time, We then call `onTimeout()` after that time has passed. // protected deadline: number = 0; @@ -973,7 +958,7 @@ class QUICConnection extends EventTarget { // const time = this.conn.timeout(); // this.logger.error(`THE TIME (${this.times}): ` + time + ' ' + new Date()); // this.times++; - + // // if (time == null) { // // Clear timeout // if (this.timer != null) this.logger.debug('timeout clearing timeout'); @@ -992,9 +977,9 @@ class QUICConnection extends EventTarget { // clearTimeout(this.timer); // delete this.timer; // this.deadline = newDeadline; - + // // this.logger.warn('BEFORE SET TIMEOUT 1: ' + time); - + // // this.timer = setTimeout(this.onTimeout, time); // } // } else { @@ -1006,9 +991,9 @@ class QUICConnection extends EventTarget { // } // this.logger.debug(`timeout creating timer with ${time} delay`); // this.deadline = newDeadline; - + // // this.logger.warn('BEFORE SET TIMEOUT 2: ' + time); - + // // this.timer = setTimeout(this.onTimeout, time); // } // } diff --git a/src/QUICStream.ts b/src/QUICStream.ts index 2c1ff305..34aabc7f 100644 --- a/src/QUICStream.ts +++ b/src/QUICStream.ts @@ -212,14 +212,14 @@ class QUICStream * 1. Top-down control flow - means explicit destruction from QUICConnection * 2. Bottom-up control flow - means stream events from users of this stream */ - public async destroy() { + public async destroy({ force = false }: { force: Boolean } ) { this.logger.info(`Destroy ${this.constructor.name}`); - if (!this._recvClosed) { + if (!this._recvClosed && force) { const e = new errors.ErrorQUICStreamClose(); this.readableController.error(e); await this.closeRecv(true, e); } - if (!this._sendClosed) { + if (!this._sendClosed && force) { const e = new errors.ErrorQUICStreamClose(); this.writableController.error(e); await this.closeSend(true, e); From bacfc4d7b0b21743dc92248a23c478e3602d5708 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Wed, 21 Jun 2023 14:22:53 +1000 Subject: [PATCH 03/22] tests: expanded native TLS tests to demonstrate custom connection failure [ci skip] --- tests/native/quiche.tls.test.ts | 2559 ++++++++++++++++++++++--------- tests/utils.ts | 26 +- 2 files changed, 1828 insertions(+), 757 deletions(-) diff --git a/tests/native/quiche.tls.test.ts b/tests/native/quiche.tls.test.ts index ab28047f..7d2e345b 100644 --- a/tests/native/quiche.tls.test.ts +++ b/tests/native/quiche.tls.test.ts @@ -5,6 +5,7 @@ import { quiche } from '@/native'; import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; import QUICConnectionId from '@/QUICConnectionId'; import * as utils from '@/utils'; +import { sleep } from '@/utils'; import * as testsUtils from '../utils'; describe('quiche tls', () => { @@ -139,16 +140,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -159,27 +157,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -190,60 +185,45 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-handshake- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('server is established', async () => { expect(serverConn.isEstablished()).toBeTrue(); @@ -252,31 +232,25 @@ describe('quiche tls', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -short-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client and server established', async () => { // Both client and server is established @@ -296,13 +270,10 @@ describe('quiche tls', () => { clientConn.onTimeout(); await testsUtils.waitForTimeoutNull(clientConn); expect(clientConn.timeout()).toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); await testsUtils.sleep(serverConn.timeout()!); serverConn.onTimeout(); await testsUtils.waitForTimeoutNull(serverConn); @@ -370,16 +341,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -390,27 +358,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -421,47 +386,35 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-handshake- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); @@ -469,21 +422,17 @@ describe('quiche tls', () => { test('client -handshake-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); // Server rejects client handshake - expect( - () => - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ) + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), ).toThrow('TlsFail'); expect(serverConn.localError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(serverConn.peerError()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -498,7 +447,7 @@ describe('quiche tls', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); expect(serverConn.timeout()).not.toBeNull(); @@ -510,13 +459,10 @@ describe('quiche tls', () => { expect(serverConn.isClosed()).toBeFalse(); // Server is in draining state now expect(serverConn.isDraining()).toBeTrue(); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); expect(clientConn.timeout()).not.toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); expect(clientConn.isInEarlyData()).toBeFalse(); @@ -597,16 +543,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -617,27 +560,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -648,60 +588,47 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-handshake- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server handshake expect(() => - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ) + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), ).toThrow('TlsFail'); expect(clientConn.localError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(clientConn.peerError()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); expect(clientConn.isInEarlyData()).toBeFalse(); expect(clientConn.isEstablished()).toBeFalse(); @@ -714,7 +641,7 @@ describe('quiche tls', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderHandshake = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); expect(clientConn.timeout()).not.toBeNull(); @@ -726,19 +653,16 @@ describe('quiche tls', () => { expect(clientConn.isClosed()).toBeFalse(); // Client is in draining state now expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(serverConn.localError()).toBeNull(); expect(serverConn.peerError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(serverConn.timeout()).not.toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -761,7 +685,7 @@ describe('quiche tls', () => { expect(serverConn.isClosed()).toBeTrue(); }); }); - describe('ECDSA success', () => { + describe('RSA custom fail verifying client', () => { // These tests run in-order, and each step is a state transition const clientHost = { host: '127.0.0.1' as Host, @@ -788,16 +712,16 @@ describe('quiche tls', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -821,16 +745,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -841,27 +762,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -872,108 +790,108 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); }); - test('client -initial-> server', async () => { + test('client -handshake-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('server is established', async () => { expect(serverConn.isEstablished()).toBeTrue(); }); - test('client <-short- server', async () => { + test('server close early', async () => { + serverConn.close(false, 304, Buffer.from('Custom TLS failed')); [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderShort = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN - ); - expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); - }); - test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderShort = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN - ); - expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); - }); - test('client and server established', async () => { - // Both client and server is established - // Server connection timeout is now null - // Note that this occurs after the server has received the last short frame - // This is due to max idle timeout of 0 - // need to check the timeout - expect(clientConn.isEstablished()).toBeTrue(); + + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + expect(serverConn.peerError()).toBeNull(); + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); expect(serverConn.isEstablished()).toBeTrue(); - expect(clientConn.timeout()).toBeNull(); - expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Should now be draining + expect(serverConn.isDraining()).toBeTrue(); + + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Should now be draining + expect(clientConn.isDraining()).toBeTrue(); }); - test('client close', async () => { - clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - await testsUtils.sleep(clientConn.timeout()!); - clientConn.onTimeout(); + test('client ends after timeout', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); await testsUtils.waitForTimeoutNull(clientConn); - expect(clientConn.timeout()).toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); - await testsUtils.sleep(serverConn.timeout()!); - serverConn.onTimeout(); - await testsUtils.waitForTimeoutNull(serverConn); - expect(serverConn.timeout()).toBeNull(); + await sleep((clientConn.timeout() ?? 0) + 1); + clientConn.onTimeout(); expect(clientConn.isClosed()).toBeTrue(); + }); + test('server ends after timeout', async () => { + expect(() => serverConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(serverConn); expect(serverConn.isClosed()).toBeTrue(); }); }); - describe('ECDSA fail verifying client', () => { + describe('RSA custom fail verifying server', () => { // These tests run in-order, and each step is a state transition const clientHost = { host: '127.0.0.1' as Host, @@ -1000,15 +918,16 @@ describe('quiche tls', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, + key: keyPairRSAPEM.privateKey, + cert: certRSAPEM, + ca: certRSAPEM, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1032,16 +951,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -1052,27 +968,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -1083,87 +996,61 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); }); - test('client -initial-> server', async () => { + test('client -handshake-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - expect(() => - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ) - ).toThrow('TlsFail'); - expect(serverConn.localError()).toEqual({ + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client close early', async () => { + clientConn.close(false, 304, Buffer.from('Custom TLS failed')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + + expect(clientConn.localError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() - }); - expect(serverConn.peerError()).toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - expect(serverConn.isDraining()).toBeFalse(); - }); - test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - const serverHeaderHandshake = quiche.Header.fromSlice( - serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN - ); - expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(serverConn.timeout()).not.toBeNull(); - expect(serverConn.isTimedOut()).toBeFalse(); - expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); - expect(serverConn.isResumed()).toBeFalse(); - expect(serverConn.isReadable()).toBeFalse(); - expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now - expect(serverConn.isDraining()).toBeTrue(); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); - expect(clientConn.localError()).toBeNull(); - expect(clientConn.peerError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array() + reason: expect.any(Uint8Array), }); + expect(clientConn.peerError()).toBeNull(); + expect(clientConn.timeout()).not.toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); expect(clientConn.isInEarlyData()).toBeFalse(); @@ -1171,221 +1058,46 @@ describe('quiche tls', () => { expect(clientConn.isResumed()).toBeFalse(); expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now + // Should now be draining expect(clientConn.isDraining()).toBeTrue(); - }); - test('client and server close', async () => { - expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); - await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); - expect(clientConn.isClosed()).toBeTrue(); - expect(serverConn.isClosed()).toBeTrue(); - }); - }); - describe('ECDSA fail verifying server', () => { - // These tests run in-order, and each step is a state transition - const clientHost = { - host: '127.0.0.1' as Host, - port: 55555 as Port, - }; - const serverHost = { - host: '127.0.0.1' as Host, - port: 55556, - }; - // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; - const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; - const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let clientQuicheConfig: Config; - let serverQuicheConfig: Config; - let clientScid: QUICConnectionId; - let clientDcid: QUICConnectionId; - let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; - let clientConn: Connection; - let serverConn: Connection; - beforeAll(async () => { - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - }; - const serverConfig: QUICConfig = { - ...serverDefault, - verifyPeer: true, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - ca: certECDSAPEM, - }; - clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); - }); - test('client connect', async () => { - // Randomly generate the client SCID - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - clientScid = new QUICConnectionId(scidBuffer); - clientConn = quiche.Connection.connect( - null, - clientScid, - clientHost, - serverHost, - clientQuicheConfig, - ); - }); - test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - }); - test('client and server negotiation', async () => { - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN - ); - clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); - serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), - 0, - quiche.MAX_CONN_ID_LEN - ); - // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); - const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const retryDatagramLength = quiche.retry( - clientScid, - clientDcid, - serverScid, - token, - clientHeaderInitial.version, - retryDatagram - ); - // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); - // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitialRetry = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN - ); - // Validate the token - const dcidOriginal = await utils.validateToken( - Buffer.from(clientHeaderInitialRetry.token!), - clientHost.host, - crypto - ); - // The original randomly generated DCID was embedded in the token - expect(dcidOriginal).toEqual(clientDcid); - }); - test('server accept', async () => { - serverConn = quiche.Connection.accept( - serverScid, - clientDcid, - serverHost, - clientHost, - serverQuicheConfig - ); - clientDcid = serverScid; - serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); - }); - test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - // Client rejects server initial - expect(() => - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ) - ).toThrow('TlsFail'); - expect(clientConn.localError()).toEqual({ - isApp: false, - // This code is unknown! - errorCode: 304, - reason: new Uint8Array() + + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, }); - expect(clientConn.peerError()).toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - expect(clientConn.isDraining()).toBeFalse(); - }); - test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - const clientHeaderInitial = quiche.Header.fromSlice( - clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN - ); - expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(clientConn.timeout()).not.toBeNull(); - expect(clientConn.isTimedOut()).toBeFalse(); - expect(clientConn.isInEarlyData()).toBeFalse(); - expect(clientConn.isEstablished()).toBeFalse(); - expect(clientConn.isResumed()).toBeFalse(); - expect(clientConn.isReadable()).toBeFalse(); - expect(clientConn.isClosed()).toBeFalse(); - // Client is in draining state now - expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + expect(serverConn.localError()).toBeNull(); expect(serverConn.peerError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: expect.any(Uint8Array), }); + expect(serverConn.timeout()).not.toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); - expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); expect(serverConn.isResumed()).toBeFalse(); expect(serverConn.isReadable()).toBeFalse(); expect(serverConn.isClosed()).toBeFalse(); - // Server is in draining state now + // Should now be draining expect(serverConn.isDraining()).toBeTrue(); }); - test('client and server close', async () => { + test('client ends after timeout', async () => { expect(() => clientConn.send(clientBuffer)).toThrow('Done'); - expect(() => serverConn.send(serverBuffer)).toThrow('Done'); - expect(clientConn.timeout()).not.toBeNull(); - expect(serverConn.timeout()).not.toBeNull(); await testsUtils.waitForTimeoutNull(clientConn); - await testsUtils.waitForTimeoutNull(serverConn); + await sleep((clientConn.timeout() ?? 0) + 1); + clientConn.onTimeout(); expect(clientConn.isClosed()).toBeTrue(); + }); + test('server ends after timeout', async () => { + expect(() => serverConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(serverConn); expect(serverConn.isClosed()).toBeTrue(); }); }); - describe('Ed25519 success', () => { + describe('ECDSA success', () => { // These tests run in-order, and each step is a state transition const clientHost = { host: '127.0.0.1' as Host, @@ -1412,16 +1124,16 @@ describe('quiche tls', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1445,16 +1157,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -1465,27 +1174,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -1496,40 +1202,31 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('server is established', async () => { expect(serverConn.isEstablished()).toBeTrue(); @@ -1538,31 +1235,25 @@ describe('quiche tls', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -short-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client and server established', async () => { // Both client and server is established @@ -1582,13 +1273,10 @@ describe('quiche tls', () => { clientConn.onTimeout(); await testsUtils.waitForTimeoutNull(clientConn); expect(clientConn.timeout()).toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); await testsUtils.sleep(serverConn.timeout()!); serverConn.onTimeout(); await testsUtils.waitForTimeoutNull(serverConn); @@ -1597,7 +1285,7 @@ describe('quiche tls', () => { expect(serverConn.isClosed()).toBeTrue(); }); }); - describe('Ed25519 fail verifying client', () => { + describe('ECDSA fail verifying client', () => { // These tests run in-order, and each step is a state transition const clientHost = { host: '127.0.0.1' as Host, @@ -1624,15 +1312,15 @@ describe('quiche tls', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1656,16 +1344,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -1676,27 +1361,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -1707,27 +1389,21 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); @@ -1735,19 +1411,16 @@ describe('quiche tls', () => { test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); expect(() => - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ) + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), ).toThrow('TlsFail'); expect(serverConn.localError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(serverConn.peerError()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -1762,7 +1435,7 @@ describe('quiche tls', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); expect(serverConn.timeout()).not.toBeNull(); @@ -1774,19 +1447,16 @@ describe('quiche tls', () => { expect(serverConn.isClosed()).toBeFalse(); // Server is in draining state now expect(serverConn.isDraining()).toBeTrue(); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); expect(clientConn.localError()).toBeNull(); expect(clientConn.peerError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(clientConn.timeout()).not.toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -1809,7 +1479,7 @@ describe('quiche tls', () => { expect(serverConn.isClosed()).toBeTrue(); }); }); - describe('Ed25519 fail verifying server', () => { + describe('ECDSA fail verifying server', () => { // These tests run in-order, and each step is a state transition const clientHost = { host: '127.0.0.1' as Host, @@ -1836,15 +1506,15 @@ describe('quiche tls', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, - key: keyPairEd25519PEM.privateKey, - cert: certEd25519PEM, - ca: certEd25519PEM, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1868,16 +1538,13 @@ describe('quiche tls', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry const token = await utils.mintToken(clientDcid, clientHost.host, crypto); @@ -1888,27 +1555,24 @@ describe('quiche tls', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); // Client will retry the initial packet with the token [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Validate the token const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -1919,41 +1583,31 @@ describe('quiche tls', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server initial expect(() => - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ) + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), ).toThrow('TlsFail'); - - expect(clientConn.localError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(clientConn.peerError()).toBeNull(); - - expect(clientConn.isTimedOut()).toBeFalse(); expect(clientConn.isInEarlyData()).toBeFalse(); expect(clientConn.isEstablished()).toBeFalse(); @@ -1966,7 +1620,7 @@ describe('quiche tls', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); expect(clientConn.timeout()).not.toBeNull(); @@ -1978,23 +1632,17 @@ describe('quiche tls', () => { expect(clientConn.isClosed()).toBeFalse(); // Client is in draining state now expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); - + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(serverConn.localError()).toBeNull(); expect(serverConn.peerError()).toEqual({ isApp: false, // This code is unknown! errorCode: 304, - reason: new Uint8Array() + reason: new Uint8Array(), }); - - expect(serverConn.timeout()).not.toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -2016,4 +1664,1413 @@ describe('quiche tls', () => { expect(serverConn.isClosed()).toBeTrue(); }); }); + describe('ECDSA custom fail verifying client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + + test('server close early', async () => { + serverConn.close(false, 304, Buffer.from('Custom TLS failed')); + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + expect(serverConn.peerError()).toBeNull(); + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Should now be draining + expect(serverConn.isDraining()).toBeTrue(); + + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Should now be draining + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client ends after timeout', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(clientConn); + await sleep((clientConn.timeout() ?? 0) + 1); + clientConn.onTimeout(); + expect(clientConn.isClosed()).toBeTrue(); + }); + test('server ends after timeout', async () => { + expect(() => serverConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('ECDSA custom fail verifying server', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairECDSAPEM.privateKey, + cert: certECDSAPEM, + ca: certECDSAPEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + + test('client close early', async () => { + clientConn.close(false, 304, Buffer.from('Custom TLS failed')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + + expect(clientConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + expect(clientConn.peerError()).toBeNull(); + + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Should now be draining + expect(clientConn.isDraining()).toBeTrue(); + + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Should now be draining + expect(serverConn.isDraining()).toBeTrue(); + }); + test('client ends after timeout', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(clientConn); + await sleep((clientConn.timeout() ?? 0) + 1); + clientConn.onTimeout(); + expect(clientConn.isClosed()).toBeTrue(); + }); + test('server ends after timeout', async () => { + expect(() => serverConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 success', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('server is established', async () => { + expect(serverConn.isEstablished()).toBeTrue(); + }); + test('client <-short- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderShort = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderShort.ty).toBe(quiche.Type.Short); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client -short-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderShort = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderShort.ty).toBe(quiche.Type.Short); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client and server established', async () => { + // Both client and server is established + // Server connection timeout is now null + // Note that this occurs after the server has received the last short frame + // This is due to max idle timeout of 0 + // need to check the timeout + expect(clientConn.isEstablished()).toBeTrue(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + }); + test('client close', async () => { + clientConn.close(true, 0, Buffer.from('')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + await testsUtils.sleep(clientConn.timeout()!); + clientConn.onTimeout(); + await testsUtils.waitForTimeoutNull(clientConn); + expect(clientConn.timeout()).toBeNull(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + await testsUtils.sleep(serverConn.timeout()!); + serverConn.onTimeout(); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.timeout()).toBeNull(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + }); + test('client is established', async () => { + expect(clientConn.isEstablished()).toBeTrue(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + expect(() => + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }), + ).toThrow('TlsFail'); + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array(), + }); + expect(serverConn.peerError()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client <-handshake- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + const serverHeaderHandshake = quiche.Header.fromSlice( + serverBuffer.subarray(0, serverSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array(), + }); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 fail verifying server', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Client rejects server initial + expect(() => + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), + ).toThrow('TlsFail'); + + expect(clientConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array(), + }); + expect(clientConn.peerError()).toBeNull(); + + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeFalse(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Client is in draining state now + expect(clientConn.isDraining()).toBeTrue(); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: new Uint8Array(), + }); + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeFalse(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Server is in draining state now + expect(serverConn.isDraining()).toBeTrue(); + }); + test('client and server close', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + expect(() => serverConn.send(serverBuffer)).toThrow('Done'); + expect(clientConn.timeout()).not.toBeNull(); + expect(serverConn.timeout()).not.toBeNull(); + await testsUtils.waitForTimeoutNull(clientConn); + await testsUtils.waitForTimeoutNull(serverConn); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 custom fail verifying client', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Client rejects server initial + expect(() => + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), + ).not.toThrow('TlsFail'); + + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toBeNull(); + + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toBeNull(); + + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('server close early', async () => { + serverConn.close(false, 304, Buffer.from('Custom TLS failed')); + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + + expect(serverConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + expect(serverConn.peerError()).toBeNull(); + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Should now be draining + expect(serverConn.isDraining()).toBeTrue(); + + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); + + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Should now be draining + expect(clientConn.isDraining()).toBeTrue(); + }); + test('client ends after timeout', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(clientConn); + await sleep((clientConn.timeout() ?? 0) + 1); + clientConn.onTimeout(); + expect(clientConn.isClosed()).toBeTrue(); + }); + test('server ends after timeout', async () => { + expect(() => serverConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); + describe('Ed25519 custom fail verifying server', () => { + // These tests run in-order, and each step is a state transition + const clientHost = { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }; + const serverHost = { + host: '127.0.0.1' as Host, + port: 55556, + }; + // These buffers will be used between the tests and will be mutated + let clientSendLength: number, clientSendInfo: SendInfo; + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let serverSendLength: number, serverSendInfo: SendInfo; + const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let clientQuicheConfig: Config; + let serverQuicheConfig: Config; + let clientScid: QUICConnectionId; + let clientDcid: QUICConnectionId; + let serverScid: QUICConnectionId; + let serverDcid: QUICConnectionId; + let clientConn: Connection; + let serverConn: Connection; + beforeAll(async () => { + const clientConfig: QUICConfig = { + ...clientDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + const serverConfig: QUICConfig = { + ...serverDefault, + verifyPeer: true, + key: keyPairEd25519PEM.privateKey, + cert: certEd25519PEM, + ca: certEd25519PEM, + }; + clientQuicheConfig = buildQuicheConfig(clientConfig); + serverQuicheConfig = buildQuicheConfig(serverConfig); + }); + test('client connect', async () => { + // Randomly generate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientQuicheConfig, + ); + }); + test('client dialing', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + }); + test('client and server negotiation', async () => { + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitialRetry = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + // Validate the token + const dcidOriginal = await utils.validateToken( + Buffer.from(clientHeaderInitialRetry.token!), + clientHost.host, + crypto, + ); + // The original randomly generated DCID was embedded in the token + expect(dcidOriginal).toEqual(clientDcid); + }); + test('server accept', async () => { + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverQuicheConfig, + ); + clientDcid = serverScid; + serverDcid = clientScid; + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + }); + test('client <-initial- server', async () => { + [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + // Client rejects server initial + expect(() => + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }), + ).not.toThrow('TlsFail'); + + expect(clientConn.localError()).toBeNull(); + expect(clientConn.peerError()).toBeNull(); + + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + }); + test('client -initial-> server', async () => { + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + expect(clientConn.isDraining()).toBeFalse(); + + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toBeNull(); + + expect(serverConn.timeout()).toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeFalse(); + }); + test('client close early', async () => { + clientConn.close(false, 304, Buffer.from('Custom TLS failed')); + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + + expect(clientConn.localError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + expect(clientConn.peerError()).toBeNull(); + + expect(clientConn.timeout()).not.toBeNull(); + expect(clientConn.isTimedOut()).toBeFalse(); + expect(clientConn.isInEarlyData()).toBeFalse(); + expect(clientConn.isEstablished()).toBeTrue(); + expect(clientConn.isResumed()).toBeFalse(); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.isClosed()).toBeFalse(); + // Should now be draining + expect(clientConn.isDraining()).toBeTrue(); + + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + expect(serverConn.localError()).toBeNull(); + expect(serverConn.peerError()).toEqual({ + isApp: false, + // This code is unknown! + errorCode: 304, + reason: expect.any(Uint8Array), + }); + + expect(serverConn.timeout()).not.toBeNull(); + expect(serverConn.isTimedOut()).toBeFalse(); + expect(serverConn.isInEarlyData()).toBeFalse(); + expect(serverConn.isEstablished()).toBeTrue(); + expect(serverConn.isResumed()).toBeFalse(); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.isClosed()).toBeFalse(); + // Should now be draining + expect(serverConn.isDraining()).toBeTrue(); + }); + test('client ends after timeout', async () => { + expect(() => clientConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(clientConn); + await sleep((clientConn.timeout() ?? 0) + 1); + clientConn.onTimeout(); + expect(clientConn.isClosed()).toBeTrue(); + }); + test('server ends after timeout', async () => { + expect(() => serverConn.send(clientBuffer)).toThrow('Done'); + await testsUtils.waitForTimeoutNull(serverConn); + expect(serverConn.isClosed()).toBeTrue(); + }); + }); }); diff --git a/tests/utils.ts b/tests/utils.ts index f429800b..6223a70b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -610,15 +610,28 @@ const handleStreamProm = async (stream: QUICStream, streamData: StreamData) => { async function waitForTimeoutNull(conn: Connection): Promise { while (true) { const timeout = conn.timeout(); - if (timeout === 0) { - await sleep(timeout); - conn.onTimeout(); - } else if (timeout == null) { - return; - } + if (timeout == null) return; + await sleep(timeout + 1); + conn.onTimeout(); } } +/** + * Creates a formatted string listing the connection state. + */ +function connStats(conn: Connection, label: string) { + return ` +----${label}---- +established: ${conn.isEstablished()}, +draining: ${conn.isDraining()}, +closed: ${conn.isClosed()}, +resumed: ${conn.isResumed()}, +earlyData: ${conn.isInEarlyData()}, +peerCerts: ${conn.peerCertChain() !== null ? 'Avaliable' : 'Missing'}, +timeout: ${conn.timeout()}, +`; +} + export { sleep, randomBytes, @@ -636,6 +649,7 @@ export { extractSocket, handleStreamProm, waitForTimeoutNull, + connStats, }; export type { Messages, StreamData }; From 9b58b85319b9d4be5b6ba3eb79dd21d21e9cbf49 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Wed, 21 Jun 2023 14:42:59 +1000 Subject: [PATCH 04/22] fix: general fixes and clean up [ci skip] --- src/QUICClient.ts | 103 +-- src/QUICConnection.ts | 324 ++++---- src/QUICServer.ts | 128 +--- src/QUICSocket.ts | 33 +- src/QUICStream.ts | 23 +- src/config.ts | 2 +- src/events.ts | 13 +- src/types.ts | 22 +- src/utils.ts | 23 +- tests/QUICServer.test.ts | 33 +- tests/config.test.ts | 32 +- .../quiche.connection.lifecycle.test.ts | 722 ++++++++---------- tests/native/quiche.test.ts | 13 +- tests/utils.ts | 1 - 14 files changed, 649 insertions(+), 823 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index f5416ddd..111dc65e 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -1,14 +1,17 @@ import type { PromiseCancellable } from '@matrixai/async-cancellable'; -import type { ContextTimed } from '@matrixai/contexts' -import type { Crypto, Host, Hostname, Port } from './types'; +import type { ContextTimed } from '@matrixai/contexts'; +import type { ClientCrypto, Host, Hostname, Port } from './types'; import type { Config } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; -import type { QUICConfig, StreamCodeToReason, StreamReasonToCode } from './types'; +import type { + QUICConfig, + StreamCodeToReason, + StreamReasonToCode, +} from './types'; import Logger from '@matrixai/logger'; import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy'; -import { destroyed, running } from '@matrixai/async-init'; +import { running } from '@matrixai/async-init'; import { timedCancellable, context } from '@matrixai/contexts/dist/decorators'; - import { quiche } from './native'; import * as utils from './utils'; import * as errors from './errors'; @@ -69,9 +72,7 @@ class QUICClient extends EventTarget { localHost?: Host | Hostname; localPort?: Port; crypto: { - ops: { - randomBytes(data: ArrayBuffer): Promise; - }; + ops: ClientCrypto; }; config?: Partial; socket?: QUICSocket; @@ -113,7 +114,7 @@ class QUICClient extends EventTarget { codeToReason?: StreamCodeToReason; logger?: Logger; }, - @context ctx: ContextTimed + @context ctx: ContextTimed, ): Promise { let address = utils.buildAddress(host, port); logger.info(`Create ${this.name} to ${address}`); @@ -133,10 +134,8 @@ class QUICClient extends EventTarget { // resolved to ::1 host_ = utils.resolvesZeroIP(host_); // This error promise is only used during `connection.start()`. - const { - p: socketErrorP, - rejectP: rejectSocketErrorP - } = utils.promise(); + const { p: socketErrorP, rejectP: rejectSocketErrorP } = + utils.promise(); const handleQUICSocketError = (e: events.QUICSocketErrorEvent) => { rejectSocketErrorP(e.detail); }; @@ -157,11 +156,9 @@ class QUICClient extends EventTarget { } isSocketShared = true; } - socket.addEventListener( - 'socketError', - handleQUICSocketError, - { once: true } - ); + socket.addEventListener('socketError', handleQUICSocketError, { + once: true, + }); // Check that the target `host` is compatible with the bound socket host if ( socket.type === 'ipv4' && @@ -212,9 +209,7 @@ class QUICClient extends EventTarget { }); try { await Promise.race([ - await connection.start( - { ...ctx, signal: abortController.signal } - ), + await connection.start({ ...ctx, signal: abortController.signal }), socketErrorP, ]); } catch (e) { @@ -247,18 +242,15 @@ class QUICClient extends EventTarget { // QUIC socket errors are re-emitted but a destroy takes place this.dispatchEvent( new events.QUICClientErrorEvent({ - detail: new errors.ErrorQUICClient( - 'Socket error', - { - cause: e.detail, - } - ) + detail: new errors.ErrorQUICClient('Socket error', { + cause: e.detail, + }), }), ); try { // Force destroy means don't destroy gracefully await this.destroy({ - force: true + force: true, }); } catch (e) { this.dispatchEvent( @@ -273,7 +265,7 @@ class QUICClient extends EventTarget { try { // Force destroy means don't destroy gracefully await this.destroy({ - force: true + force: true, }); } catch (e) { this.dispatchEvent( @@ -290,22 +282,21 @@ class QUICClient extends EventTarget { /** * This must not throw any exceptions. */ - protected handleQUICConnectionEvents = async (e: events.QUICConnectionEvent) => { + protected handleQUICConnectionEvents = async ( + e: events.QUICConnectionEvent, + ) => { if (e instanceof events.QUICConnectionErrorEvent) { this.dispatchEvent( new events.QUICClientErrorEvent({ - detail: new errors.ErrorQUICClient( - 'Connection error', - { - cause: e.detail, - } - ) + detail: new errors.ErrorQUICClient('Connection error', { + cause: e.detail, + }), }), ); try { // Force destroy means don't destroy gracefully await this.destroy({ - force: true + force: true, }); } catch (e) { this.dispatchEvent( @@ -318,7 +309,7 @@ class QUICClient extends EventTarget { try { // Force destroy means don't destroy gracefully await this.destroy({ - force: true + force: true, }); } catch (e) { this.dispatchEvent( @@ -349,30 +340,24 @@ class QUICClient extends EventTarget { this.isSocketShared = isSocketShared; this._connection = connection; // Listen on all socket events - socket.addEventListener( - 'socketError', - this.handleQUICSocketEvents - ); - socket.addEventListener( - 'socketStop', - this.handleQUICSocketEvents - ); + socket.addEventListener('socketError', this.handleQUICSocketEvents); + socket.addEventListener('socketStop', this.handleQUICSocketEvents); // Listen on all connection events connection.addEventListener( 'connectionStream', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); connection.addEventListener( 'connectionStop', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); connection.addEventListener( 'connectionError', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); connection.addEventListener( 'streamDestroy', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); } @@ -408,30 +393,24 @@ class QUICClient extends EventTarget { const address = utils.buildAddress(this.socket.host, this.socket.port); this.logger.info(`Destroy ${this.constructor.name} on ${address}`); // Listen on all socket events - this.socket.removeEventListener( - 'socketError', - this.handleQUICSocketEvents - ); - this.socket.removeEventListener( - 'socketStop', - this.handleQUICSocketEvents - ); + this.socket.removeEventListener('socketError', this.handleQUICSocketEvents); + this.socket.removeEventListener('socketStop', this.handleQUICSocketEvents); // Listen on all connection events this.connection.removeEventListener( 'connectionStream', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); this.connection.removeEventListener( 'connectionStop', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); this.connection.removeEventListener( 'connectionError', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); this.connection.removeEventListener( 'streamDestroy', - this.handleQUICConnectionEvents + this.handleQUICConnectionEvents, ); await this._connection.stop({ force }); if (!this.isSocketShared) { diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index fda514d3..9a2ee2e1 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -1,25 +1,28 @@ import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ContextTimed } from '@matrixai/contexts'; import type QUICSocket from './QUICSocket'; -import type QUICConnectionMap from './QUICConnectionMap'; import type QUICConnectionId from './QUICConnectionId'; import type { Host, Port, RemoteInfo, StreamId } from './types'; import type { Connection, ConnectionErrorCode, SendInfo } from './native/types'; import type { StreamCodeToReason, StreamReasonToCode } from './types'; -import type { QUICConfig, ConnectionMetadata } from './types'; -import { StartStop, ready, status, running } from '@matrixai/async-init/dist/StartStop'; +import type { QUICConfig } from './types'; +import { Monitor, RWLockWriter } from '@matrixai/async-locks'; +import { + StartStop, + ready, + status, + running, +} from '@matrixai/async-init/dist/StartStop'; import Logger from '@matrixai/logger'; -import { Lock, LockBox, Monitor, RWLockWriter } from '@matrixai/async-locks'; -import { destroyed } from '@matrixai/async-init'; +import { Lock, LockBox } from '@matrixai/async-locks'; import { Timer } from '@matrixai/timer'; +import { context, timedCancellable } from '@matrixai/contexts/dist/decorators'; import { buildQuicheConfig } from './config'; import QUICStream from './QUICStream'; import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; import * as errors from './errors'; -import { promise } from './utils'; -import { context, timedCancellable } from '@matrixai/contexts/dist/decorators'; /** * Think of this as equivalent to `net.Socket`. @@ -144,6 +147,7 @@ class QUICConnection extends EventTarget { */ protected _remotePort: Port; + // TODO: use it or loose it? /** * Bubble up all QUIC stream events. */ @@ -186,8 +190,8 @@ class QUICConnection extends EventTarget { protected lastErrorMessage?: string; - protected lockbox = new LockBox(); - protected readonly lockCode = 'Lock'; + public readonly lockbox = new LockBox(); + public readonly lockCode = 'Lock'; // TODO: more unique code public constructor({ type, @@ -199,32 +203,34 @@ class QUICConnection extends EventTarget { reasonToCode = () => 0, codeToReason = (type, code) => new Error(`${type} ${code}`), logger, - }: { - type: 'client'; - scid: QUICConnectionId; - dcid?: undefined; - remoteInfo: RemoteInfo; - config: QUICConfig; - socket: QUICSocket; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - logger?: Logger; - } | { - type: 'server'; - scid: QUICConnectionId; - dcid: QUICConnectionId; - remoteInfo: RemoteInfo; - config: QUICConfig; - socket: QUICSocket; - reasonToCode?: StreamReasonToCode; - codeToReason?: StreamCodeToReason; - logger?: Logger; - }) { + }: + | { + type: 'client'; + scid: QUICConnectionId; + dcid?: undefined; + remoteInfo: RemoteInfo; + config: QUICConfig; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + logger?: Logger; + } + | { + type: 'server'; + scid: QUICConnectionId; + dcid: QUICConnectionId; + remoteInfo: RemoteInfo; + config: QUICConfig; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + logger?: Logger; + }) { super(); this.logger = logger ?? new Logger(`${this.constructor.name} ${scid}`); const quicheConfig = buildQuicheConfig(config); let conn: Connection; - if (type === 'client') { + if (type === 'client') { // This message will be connected to the `this.start` this.logger.info(`Connect ${this.constructor.name}`); conn = quiche.Connection.connect( @@ -277,7 +283,6 @@ class QUICConnection extends EventTarget { } = utils.promise(); this.establishedP = establishedP; this.resolveEstablishedP = () => { - // Upon the first time you call this, it is now true // But prior to this @@ -293,12 +298,10 @@ class QUICConnection extends EventTarget { // Cause it is `void` here // I think it's better to use the `resolveEstablishedP` - - const { p: secureEstablishedP, resolveP: resolveSecureEstablishedP, - rejectP: rejectSecureEstablishedP + rejectP: rejectSecureEstablishedP, } = utils.promise(); this.secureEstablishedP = secureEstablishedP; this.resolveSecureEstablishedP = resolveSecureEstablishedP; @@ -354,9 +357,7 @@ class QUICConnection extends EventTarget { // After this is done // We need to established the keep alive interval time if (this.config.keepAliveIntervalTime != null) { - this.startKeepAliveIntervalTimer( - this.config.keepAliveIntervalTime - ); + this.startKeepAliveIntervalTimer(this.config.keepAliveIntervalTime); } // Do we remove the on abort event listener? // I forgot... @@ -380,31 +381,35 @@ class QUICConnection extends EventTarget { applicationError = true, errorCode = 0, errorMessage = '', - force = false - }: { - applicationError?: false; - errorCode?: ConnectionErrorCode; - errorMessage?: string; - force?: boolean; - } | { - applicationError: true; - errorCode?: number; - errorMessage?: string; - force?: boolean; - }= {}, - mon: Monitor, + force = false, + }: + | { + applicationError?: false; + errorCode?: ConnectionErrorCode; + errorMessage?: string; + force?: boolean; + } + | { + applicationError: true; + errorCode?: number; + errorMessage?: string; + force?: boolean; + } = {}, + mon?: Monitor, ) { this.logger.info(`Stop ${this.constructor.name}`); - await mon.withF(this.lockCode, async () => { - + // Cleaning up existing streams const streamDestroyPs: Array> = []; for (const stream of this.streamMap.values()) { streamDestroyPs.push(stream.destroy({ force })); } await Promise.all(streamDestroyPs); - // Do we do this afterwards or before? + // Do we do this afterward or before? + // FIXME: not sure it really matters, don't really need a keep alive while stopping. this.stopKeepAliveIntervalTimer(); try { + mon = mon ?? new Monitor(this.lockbox, RWLockWriter); + await mon.withF(this.lockCode, async (mon) => { // If this is already closed, then `Done` will be thrown // Otherwise it can send `CONNECTION_CLOSE` frame // This can be 0x1c close at the QUIC layer or no errors @@ -416,7 +421,8 @@ class QUICConnection extends EventTarget { this.conn.close(applicationError, errorCode, Buffer.from(errorMessage)); // If we get a `Done` exception we don't bother calling send // The send only gets sent if the `Done` is not the case - await this.send(); + await this.send(mon); + }); } catch (e) { // If the connection is already closed, `Done` will be thrown if (e.message !== 'Done') { @@ -425,6 +431,9 @@ class QUICConnection extends EventTarget { } } + if (this.conn.isClosed()) { + this.resolveClosedP(); + } // Now we await for the closedP await this.closedP; @@ -435,7 +444,6 @@ class QUICConnection extends EventTarget { this.socket.connectionMap.delete(this.connectionId); this.dispatchEvent(new events.QUICConnectionStopEvent()); - }); this.logger.info(`Stopped ${this.constructor.name}`); } @@ -460,11 +468,21 @@ class QUICConnection extends EventTarget { * Any errors must be emitted as events. * @internal */ - public recv(data: Uint8Array, remoteInfo: RemoteInfo) { + public async recv( + data: Uint8Array, + remoteInfo: RemoteInfo, + mon: Monitor, + ) { + if (!mon.isLocked(this.lockCode)) { + return mon.withF(this.lockCode, async (mon) => { + return this.recv(data, remoteInfo, mon); + }); + } + try { // The remote information may be changed on each receive // However to do so would mean connection migration, - // which is is not yet supported + // which is not yet supported this._remoteHost = remoteInfo.host; this._remotePort = remoteInfo.port; const recvInfo = { @@ -502,23 +520,18 @@ class QUICConnection extends EventTarget { if (this.conn.isEstablished()) { this.resolveEstablishedP(); - if (this.type === 'server') { // For server connections, if we are established // we are secure established this.resolveSecureEstablishedP(); } else if (this.type === 'client') { - // We need a hueristic to indicate whether we are securely established - // If we are already established // AND IF, we are getting a packet after establishment // And we didn't result in an error // Neither draining, nor closed, nor timed out - // For server connections // If we are already established, then we are secure established - // To know if the server is also established // We need to know the NEXT recv after we are already established // So we received something, and that allows us to be established @@ -532,7 +545,6 @@ class QUICConnection extends EventTarget { // But we must only switch // If were "already" established // That this wasn't the first time we were established - } } @@ -543,14 +555,11 @@ class QUICConnection extends EventTarget { // What if we were "concatenated" packet // Then it could be a problem right? - if (this.conn.isClosed()) { this.resolveClosedP(); return; } - - if (this.conn.isInEarlyData() || this.conn.isEstablished()) { const readIds: Array = []; for (const streamId of this.conn.readable() as Iterable) { @@ -562,7 +571,7 @@ class QUICConnection extends EventTarget { connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - // maxReadableStreamBytes: this.maxReadableStreamBytes, + // MaxReadableStreamBytes: this.maxReadableStreamBytes, // maxWritableStreamBytes: this.maxWritableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), }); @@ -572,7 +581,7 @@ class QUICConnection extends EventTarget { } readIds.push(quicStream.streamId); quicStream.read(); - quicStream.dispatchEvent(new events.QUICStreamReadableEvent()); + // QuicStream.dispatchEvent(new events.QUICStreamReadableEvent()); // TODO: remove? } if (readIds.length > 0) { this.logger.info(`processed reads for ${readIds}`); @@ -587,14 +596,14 @@ class QUICConnection extends EventTarget { connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - // maxReadableStreamBytes: this.maxReadableStreamBytes, + // MaxReadableStreamBytes: this.maxReadableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), }); this.dispatchEvent( new events.QUICConnectionStreamEvent({ detail: quicStream }), ); } - quicStream.dispatchEvent(new events.QUICStreamWritableEvent()); + // QuicStream.dispatchEvent(new events.QUICStreamWritableEvent()); // TODO: remove? writeIds.push(quicStream.streamId); quicStream.write(); } @@ -603,10 +612,10 @@ class QUICConnection extends EventTarget { } } } finally { - this.garbageCollectStreams('recv'); + // This.garbageCollectStreams('recv'); // FIXME: this was removed? How is this handled now? this.logger.debug('RECV FINALLY'); // Set the timeout - this.checkTimeout(); + this.setConnTimeOutTimer(); // FIXME: Might not be needed here, Only need it after calling send // If this call wasn't executed in the midst of a destroy // and yet the connection is closed or is draining, then // we need to destroy this connection @@ -616,7 +625,7 @@ class QUICConnection extends EventTarget { ) { this.logger.debug('CALLING DESTROY 2'); // Destroy in the background, we still need to process packets - void this.destroy().catch(() => {}); + void this.stop({}, mon).catch(() => {}); } } } @@ -642,65 +651,78 @@ class QUICConnection extends EventTarget { * Any errors must be emitted as events. * @internal */ - public async send(mon: Monitor): Promise { - await mon.withF(this.lockCode, async () => { - const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE); - let sendLength: number; - let sendInfo: SendInfo; - try { - // Send until `Done` - while (true) { - try { - [sendLength, sendInfo] = this.conn.send(sendBuffer); - } catch (e) { - if (e.message === 'Done') { - break; - } - throw e; + public async send( + mon: Monitor = new Monitor( + this.lockbox, + RWLockWriter, + ), + ): Promise { + if (!mon.isLocked(this.lockCode)) { + return mon.withF(this.lockCode, async (mon) => { + return this.send(mon); + }); + } + + const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE); + let sendLength: number; + let sendInfo: SendInfo; + try { + // Send until `Done` + while (true) { + try { + [sendLength, sendInfo] = this.conn.send(sendBuffer); + } catch (e) { + if (e.message === 'Done') { + break; } - await this.socket.send( - sendBuffer, - 0, - sendLength, - sendInfo.to.port, - sendInfo.to.host, - ); + throw e; } - } catch (e) { - - // If called `stop` due to an error here - // we MUST not call `this.send` again - // in fact, we do a hard-stop - // There's no need to even have a timeout at all - // Remember this exception COULD be due to `e` - // It could be due to `localError` or `remoteError` - // All of this is possbile - // Generally at least one of them is the reason - - // the error has to be one or the other - - await this.stop({ - error: e - }); - - // We need to finish without any exceptions - return; + await this.socket.send( + sendBuffer, + 0, + sendLength, + sendInfo.to.port, + sendInfo.to.host, + ); } - if (this.conn.isClosed()) { - - // But if it is closed with no error - // Then we just have to proceed! - // Plus if we are called here - - await this.stop({ - error: this.conn.localError() ?? this.conn.remoteError(), - }); + } catch (e) { + // If called `stop` due to an error here + // we MUST not call `this.send` again + // in fact, we do a hard-stop + // There's no need to even have a timeout at all + // Remember this exception COULD be due to `e` + // It could be due to `localError` or `remoteError` + // All of this is possible + // Generally at least one of them is the reason + + // the error has to be one or the other + + await this.stop( + { + applicationError: true, + errorCode: 0, // TODO: actual code? use code mapping? + errorMessage: e.message, + }, + mon, + ); - } else { - // In all other cases, reset the conn timer - this.setConnTimeOutTimer(); - } - }); + // We need to finish without any exceptions + return; + } + if (this.conn.isClosed()) { + // But if it is closed with no error + // Then we just have to proceed! + // Plus if we are called here + this.resolveClosedP(); + + await this.stop( + this.conn.localError() ?? this.conn.peerError() ?? {}, + mon, + ); + } else { + // In all other cases, reset the conn timer + this.setConnTimeOutTimer(); + } } protected setConnTimeOutTimer(): void { @@ -722,7 +744,6 @@ class QUICConnection extends EventTarget { // But if it is is timed out due to idle we raise an error if (this.conn.isTimedOut()) { - // This is just a dispatch on the connection error // Note that this may cause the client to attempt // to stop the socket and stuff @@ -735,8 +756,8 @@ class QUICConnection extends EventTarget { this.dispatchEvent( new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionTimeout() - }) + detail: new errors.ErrorQUICConnectionIdleTimeOut(), + }), ); } @@ -759,22 +780,28 @@ class QUICConnection extends EventTarget { // But also that if the status is starting // But also if we are starting // Resolve the closeP - // is technicaly an error! - - await this.stop(); + // is technically an error! + const mon = new Monitor(this.lockbox, RWLockWriter); + await this.stop( + this.conn.localError() ?? this.conn.peerError() ?? {}, + mon, + ); } // Finish return; } + const mon = new Monitor(this.lockbox, RWLockWriter); + await this.send(mon); + // Note that a `0` timeout is still a valid timeout const timeout = this.conn.timeout(); // If this is `null`, then technically there's nothing to do if (timeout == null) return; this.connTimeOutTimer = new Timer({ delay: timeout, - handler: connTimeOutHandler + handler: connTimeOutHandler, }); }; // Note that a `0` timeout is still a valid timeout @@ -787,7 +814,7 @@ class QUICConnection extends EventTarget { } this.connTimeOutTimer = new Timer({ delay: timeout, - handler: connTimeOutHandler + handler: connTimeOutHandler, }); } @@ -803,18 +830,17 @@ class QUICConnection extends EventTarget { // Intelligently schedule a PING frame. // If the connection has already sent ack-eliciting frames // then this is a noop. - await this.connLock.withF(async () => { - this.conn.sendAckEliciting(); - await this.send(); - }); + const mon = new Monitor(this.lockbox, RWLockWriter); + this.conn.sendAckEliciting(); + await this.send(mon); this.keepAliveIntervalTimer = new Timer({ delay: ms, - handler: keepAliveHandler + handler: keepAliveHandler, }); }; this.keepAliveIntervalTimer = new Timer({ delay: ms, - handler: keepAliveHandler + handler: keepAliveHandler, }); } @@ -825,14 +851,6 @@ class QUICConnection extends EventTarget { this.keepAliveIntervalTimer?.cancel(); } - - - - - - - - /** * Creates a new stream on the connection. * Only supports bidi streams atm. @@ -840,11 +858,9 @@ class QUICConnection extends EventTarget { */ @ready(new errors.ErrorQUICConnectionNotRunning()) public async streamNew(streamType: 'bidi' = 'bidi'): Promise { - // You wouldn't want AsyncMonitor here // The problem is that we want re-entrant contexts - // Technically you can do concurrent bidi and uni style streams // but no support for uni streams yet // So we don't bother with it @@ -876,7 +892,7 @@ class QUICConnection extends EventTarget { connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - // maxReadableStreamBytes: this.maxReadableStreamBytes, + // MaxReadableStreamBytes: this.maxReadableStreamBytes, // maxWritableStreamBytes: this.maxWritableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId!}`), }); diff --git a/src/QUICServer.ts b/src/QUICServer.ts index 1117bd2f..281a4c5c 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -1,17 +1,15 @@ import type { - Crypto, Host, Hostname, Port, - PromiseDeconstructed, RemoteInfo, StreamCodeToReason, StreamReasonToCode, QUICConfig, + ServerCrypto, } from './types'; import type { Header } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; -import type { QUICServerConnectionEvent } from './events'; import Logger from '@matrixai/logger'; import { running } from '@matrixai/async-init'; import { ready, StartStop } from '@matrixai/async-init/dist/StartStop'; @@ -21,7 +19,6 @@ import QUICConnectionId from './QUICConnectionId'; import QUICConnection from './QUICConnection'; import { quiche } from './native'; import * as utils from './utils'; -import { promise } from './utils'; import * as errors from './errors'; import QUICSocket from './QUICSocket'; @@ -48,20 +45,12 @@ class QUICServer extends EventTarget { protected logger: Logger; protected crypto: { key: ArrayBuffer; - ops: { - sign(key: ArrayBuffer, data: ArrayBuffer): Promise; - verify( - key: ArrayBuffer, - data: ArrayBuffer, - sig: ArrayBuffer, - ): Promise; - }; + ops: ServerCrypto; }; protected config: QUICConfig; protected socket: QUICSocket; protected reasonToCode: StreamReasonToCode | undefined; protected codeToReason: StreamCodeToReason | undefined; - protected keepaliveIntervalTime?: number | undefined; protected connectionMap: QUICConnectionMap; protected handleQUICSocketEvents = (e: events.QUICSocketEvent) => { @@ -86,7 +75,6 @@ class QUICServer extends EventTarget { resolveHostname = utils.resolveHostname, reasonToCode, codeToReason, - keepaliveIntervalTime, logger, }: { crypto: { @@ -108,7 +96,6 @@ class QUICServer extends EventTarget { resolveHostname?: (hostname: Hostname) => Host | PromiseLike; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; - keepaliveIntervalTime?: number; logger?: Logger; }) { super(); @@ -135,7 +122,6 @@ class QUICServer extends EventTarget { this.config = quicConfig; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; - this.keepaliveIntervalTime = keepaliveIntervalTime; } @ready(new errors.ErrorQUICServerNotRunning()) @@ -179,14 +165,8 @@ class QUICServer extends EventTarget { } // Register on all socket events - this.socket.addEventListener( - 'socketError', - this.handleQUICSocketEvents - ); - this.socket.addEventListener( - 'socketStop', - this.handleQUICSocketEvents - ); + this.socket.addEventListener('socketError', this.handleQUICSocketEvents); + this.socket.addEventListener('socketStop', this.handleQUICSocketEvents); this.logger.info(`Started ${this.constructor.name} on ${address}`); } @@ -199,21 +179,29 @@ class QUICServer extends EventTarget { }: { force?: boolean; } = {}) { - // console.time('destroy conn'); + // Console.time('destroy conn'); const address = utils.buildAddress(this.socket.host, this.socket.port); this.logger.info(`Stop ${this.constructor.name} on ${address}`); const destroyProms: Array> = []; for (const connection of this.connectionMap.serverConnections.values()) { - destroyProms.push(connection.destroy({ force })); + destroyProms.push( + connection.stop({ + applicationError: true, + errorMessage: 'cleaning up connections', + errorCode: 42, + force, + }), + ); // TODO: fill in with proper details } await Promise.all(destroyProms); - // console.timeEnd('destroy conn'); + // Console.timeEnd('destroy conn'); this.socket.deregisterServer(this); if (!this.isSocketShared) { // If the socket is not shared, then it can be stopped await this.socket.stop(); - this.socket.removeEventListener('error', this.handleQUICSocketError); } + this.socket.removeEventListener('socketError', this.handleQUICSocketEvents); + this.socket.removeEventListener('socketStop', this.handleQUICSocketEvents); this.dispatchEvent(new events.QUICServerStopEvent()); this.logger.info(`Stopped ${this.constructor.name} on ${address}`); } @@ -233,7 +221,6 @@ class QUICServer extends EventTarget { header: Header, dcid: QUICConnectionId, ): Promise { - // If the packet is not an `Initial` nor `ZeroRTT` then we discard the // packet. if ( @@ -245,17 +232,13 @@ class QUICServer extends EventTarget { // Derive the new connection's SCID from the client generated DCID const scid = new QUICConnectionId( - await this.crypto.ops.sign( - this.crypto.key, - dcid, - ), + await this.crypto.ops.sign(this.crypto.key, dcid), 0, quiche.MAX_CONN_ID_LEN, ); const peerAddress = utils.buildAddress(remoteInfo.host, remoteInfo.port); - // Version Negotiation if (!quiche.versionIsSupported(header.version)) { this.logger.debug( @@ -337,13 +320,14 @@ class QUICServer extends EventTarget { return; } // Here we shall re-use the originally-derived DCID as the SCID - scid = new QUICConnectionId(header.dcid); + const newScid = new QUICConnectionId(header.dcid); this.logger.debug( `Accepting new connection from QUIC packet from ${remoteInfo.host}:${remoteInfo.port}`, ); const clientConnRef = Buffer.from(header.scid).toString('hex').slice(32); - const connection = await QUICConnection.acceptQUICConnection({ - scid, + const connection = new QUICConnection({ + type: 'server', + scid: newScid, dcid: dcidOriginal, socket: this.socket, remoteInfo, @@ -354,8 +338,7 @@ class QUICServer extends EventTarget { `${QUICConnection.name} ${scid.toString().slice(32)}-${clientConnRef}`, ), }); - connection.setKeepAlive(this.keepaliveIntervalTime); - + await connection.start(); // TODO: pass ctx this.dispatchEvent( new events.QUICServerConnectionEvent({ detail: connection }), ); @@ -381,73 +364,6 @@ class QUICServer extends EventTarget { }; } - /** - * This initiates sending UDP packets to a target client to open up a port in the NAT for the client to connect - * through. This will return early if the connection already exists or was established while polling. - */ - public async initHolePunch( - remoteInfo: RemoteInfo, - timeout: number = 5000, - ): Promise { - // Checking existing connections - for (const [, connection] of this.connectionMap.serverConnections) { - if ( - remoteInfo.host === connection.remoteHost && - remoteInfo.port === connection.remotePort - ) { - // Connection exists, return early - return true; - } - } - // We need to send a random data packet to the target until the process times out or a connection is established - let timedOut = false; - const timedOutProm = promise(); - const timeoutTimer = setTimeout(() => { - timedOut = true; - timedOutProm.resolveP(); - }, timeout); - let delay = 250; - let delayTimer: NodeJS.Timer | undefined; - let sleepProm: PromiseDeconstructed | undefined; - let established = false; - const establishedProm = promise(); - // Setting up established event checking - const handleEstablished = (event: QUICServerConnectionEvent) => { - const connection = event.detail; - if ( - remoteInfo.host === connection.remoteHost && - remoteInfo.port === connection.remotePort - ) { - // Clean up and resolve - this.removeEventListener('connection', handleEstablished); - established = true; - establishedProm.resolveP(); - } - }; - this.addEventListener('connection', handleEstablished); - try { - while (!established && !timedOut) { - const message = new ArrayBuffer(32); - await this.crypto.ops.randomBytes(message); - await this.socket.send( - Buffer.from(message), - remoteInfo.port, - remoteInfo.host, - ); - sleepProm = promise(); - delayTimer = setTimeout(() => sleepProm!.resolveP(), delay); - delay *= 2; - await Promise.race([sleepProm.p, establishedProm.p, timedOutProm.p]); - } - return established; - } finally { - clearTimeout(timeoutTimer); - if (delayTimer != null) clearTimeout(delayTimer); - sleepProm?.resolveP(); - this.removeEventListener('connection', handleEstablished); - } - } - /** * Creates a retry token. * This will embed peer host IP and DCID into the token. diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index 80fb0a63..dc879647 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -1,11 +1,12 @@ import type QUICServer from './QUICServer'; import type QUICConnection from './QUICConnection'; -import type { Crypto, Host, Hostname, Port } from './types'; +import type { Host, Hostname, Port } from './types'; import type { Header } from './native/types'; import dgram from 'dgram'; import Logger from '@matrixai/logger'; -import { status, running, destroyed } from '@matrixai/async-init'; +import { running } from '@matrixai/async-init'; import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; +import { Monitor, RWLockWriter } from '@matrixai/async-locks'; import QUICConnectionId from './QUICConnectionId'; import QUICConnectionMap from './QUICConnectionMap'; import { quiche } from './native'; @@ -37,18 +38,6 @@ class QUICSocket extends EventTarget { protected socketClose: () => Promise; protected socketSend: (...params: Array) => Promise; - protected crypto?: { - key: ArrayBuffer; - ops: { - sign(key: ArrayBuffer, data: ArrayBuffer): Promise; - verify( - key: ArrayBuffer, - data: ArrayBuffer, - sig: ArrayBuffer, - ): Promise; - }; - }; - /** * Handle the datagram from UDP socket * The `data` buffer could be multiple coalesced QUIC packets. @@ -71,10 +60,7 @@ class QUICSocket extends EventTarget { } catch (e) { // `BufferTooShort` and `InvalidPacket` means that this is not a QUIC // packet. If so, then we just ignore the packet. - if ( - e.message !== 'BufferTooShort' && - e.message !== 'InvalidPacket' - ) { + if (e.message !== 'BufferTooShort' && e.message !== 'InvalidPacket') { // Emit error if it is not a `BufferTooShort` or `InvalidPacket` error. // This would indicate something went wrong in header parsing. // This is not a critical error, but should be checked. @@ -119,11 +105,12 @@ class QUICSocket extends EventTarget { } // Acquire the conn lock, this ensures mutual exclusion // for state changes on the internal connection - await connection.connLock.withF(async () => { + const mon = new Monitor(connection.lockbox, RWLockWriter); + await mon.withF(connection.lockCode, async (mon) => { // Even if we are `stopping`, the `quiche` library says we need to // continue processing any packets. - connection.recv(data, remoteInfo_); - await connection.send(); + await connection.recv(data, remoteInfo_, mon); + await connection.send(mon); }); }; @@ -257,7 +244,9 @@ class QUICSocket extends EventTarget { * If force is true, it will skip checking connections and stop the socket. * @param force - Will force the socket to end even if there are active connections, used for cleaning up after tests. */ - public async stop({ force = false }: { force?: boolean } = {}): Promise { + public async stop({ + force = false, + }: { force?: boolean } = {}): Promise { const address = utils.buildAddress(this._host, this._port); this.logger.info(`Stop ${this.constructor.name} on ${address}`); if (!force && this.connectionMap.size > 0) { diff --git a/src/QUICStream.ts b/src/QUICStream.ts index 34aabc7f..4fd28deb 100644 --- a/src/QUICStream.ts +++ b/src/QUICStream.ts @@ -7,7 +7,16 @@ import type { ConnectionMetadata, } from './types'; import type { Connection } from './native/types'; -import { ReadableStream, WritableStream, ByteLengthQueuingStrategy } from 'stream/web'; +import type { + ReadableWritablePair, + ReadableStreamDefaultController, + WritableStreamDefaultController, +} from 'stream/web'; +import { + ReadableStream, + WritableStream, + ByteLengthQueuingStrategy, +} from 'stream/web'; import Logger from '@matrixai/logger'; import { CreateDestroy, @@ -134,7 +143,7 @@ class QUICStream }, new ByteLengthQueuingStrategy({ highWaterMark: 0, - // highWaterMark: maxReadableStreamBytes, + // HighWaterMark: maxReadableStreamBytes, }), ); @@ -174,7 +183,7 @@ class QUICStream }, new ByteLengthQueuingStrategy({ highWaterMark: 0, - // highWaterMark: maxWritableStreamBytes, + // HighWaterMark: maxWritableStreamBytes, }), ); } @@ -195,7 +204,8 @@ class QUICStream * Connection information including hosts, ports and cert data. */ public get remoteInfo(): ConnectionMetadata { - return this.connection.remoteInfo; + throw Error('TMP IMP'); + // Return this.connection.remoteInfo; } /** @@ -203,7 +213,8 @@ class QUICStream * This strictly exists to work with agnostic RPC stream interface. */ public get meta(): ConnectionMetadata { - return this.connection.remoteInfo; + throw Error('TMP IMP'); + // Return this.connection.remoteInfo; } /** @@ -212,7 +223,7 @@ class QUICStream * 1. Top-down control flow - means explicit destruction from QUICConnection * 2. Bottom-up control flow - means stream events from users of this stream */ - public async destroy({ force = false }: { force: Boolean } ) { + public async destroy({ force = false }: { force?: boolean } = {}) { this.logger.info(`Destroy ${this.constructor.name}`); if (!this._recvClosed && force) { const e = new errors.ErrorQUICStreamClose(); diff --git a/src/config.ts b/src/config.ts index 7dca8a84..5c9a1249 100644 --- a/src/config.ts +++ b/src/config.ts @@ -156,7 +156,7 @@ function buildQuicheConfig(config: QUICConfig): QuicheConfig { } catch (e) { throw new errors.ErrorQUICConfig( `Failed to build Quiche config with custom SSL context: ${e.message}`, - { cause: e } + { cause: e }, ); } if (config.logKeys != null) { diff --git a/src/events.ts b/src/events.ts index 0c42da9a..925d752f 100644 --- a/src/events.ts +++ b/src/events.ts @@ -3,7 +3,7 @@ import type QUICStream from './QUICStream'; // Socket events -abstract class QUICSocketEvent extends Event {}; +abstract class QUICSocketEvent extends Event {} class QUICSocketStartEvent extends Event { constructor(options?: EventInit) { @@ -31,7 +31,7 @@ class QUICSocketErrorEvent extends Event { // Client events -abstract class QUICClientEvent extends Event {}; +abstract class QUICClientEvent extends Event {} class QUICClientDestroyEvent extends Event { constructor(options?: EventInit) { @@ -53,7 +53,7 @@ class QUICClientErrorEvent extends Event { // Server events -abstract class QUICServerEvent extends Event {}; +abstract class QUICServerEvent extends Event {} class QUICServerConnectionEvent extends Event { public detail: QUICConnection; @@ -93,7 +93,7 @@ class QUICServerErrorEvent extends Event { // Connection events -abstract class QUICConnectionEvent extends Event {}; +abstract class QUICConnectionEvent extends Event {} class QUICConnectionStreamEvent extends QUICConnectionEvent { public detail: QUICStream; @@ -133,7 +133,7 @@ class QUICConnectionErrorEvent extends QUICConnectionEvent { // Stream events -abstract class QUICStreamEvent extends Event {}; +abstract class QUICStreamEvent extends Event {} class QUICStreamDestroyEvent extends QUICStreamEvent { constructor(options?: EventInit) { @@ -149,19 +149,16 @@ export { QUICClientEvent, QUICClientDestroyEvent, QUICClientErrorEvent, - QUICServerEvent, QUICServerConnectionEvent, QUICServerStartEvent, QUICServerStopEvent, QUICServerErrorEvent, - QUICConnectionEvent, QUICConnectionStreamEvent, QUICConnectionStartEvent, QUICConnectionStopEvent, QUICConnectionErrorEvent, - QUICStreamEvent, QUICStreamDestroyEvent, }; diff --git a/src/types.ts b/src/types.ts index 8f386eb5..22e78030 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,10 +38,10 @@ type ConnectionId = Opaque<'ConnectionId', Buffer>; type ConnectionIdString = Opaque<'ConnectionIdString', string>; /** - * Crypto utility object - * Remember ever Node Buffer is an ArrayBuffer + * Client crypto utility object + * Remember every Node Buffer is an ArrayBuffer */ -type Crypto = { +type ClientCrypto = { sign(key: ArrayBuffer, data: ArrayBuffer): Promise; verify( key: ArrayBuffer, @@ -51,6 +51,19 @@ type Crypto = { randomBytes(data: ArrayBuffer): Promise; }; +/** + * Server crypto utility object + * Remember every Node Buffer is an ArrayBuffer + */ +type ServerCrypto = { + sign(key: ArrayBuffer, data: ArrayBuffer): Promise; + verify( + key: ArrayBuffer, + data: ArrayBuffer, + sig: ArrayBuffer, + ): Promise; +}; + type StreamId = Opaque<'StreamId', number>; /** @@ -298,7 +311,8 @@ export type { PromiseDeconstructed, ConnectionId, ConnectionIdString, - Crypto, + ClientCrypto, + ServerCrypto, StreamId, Host, Hostname, diff --git a/src/utils.ts b/src/utils.ts index a7cb7a14..979f3e0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,11 +5,11 @@ import type { ConnectionIdString, Host, Hostname, - Crypto + ServerCrypto, } from './types'; -import QUICConnectionId from './QUICConnectionId'; import dns from 'dns'; import { IPv4, IPv6, Validator } from 'ip-num'; +import QUICConnectionId from './QUICConnectionId'; import * as errors from './errors'; /** @@ -323,17 +323,15 @@ function certificatePEMsToCertChainPem(pems: Array): string { async function mintToken( dcid: QUICConnectionId, peerHost: Host, - crypto:{ + crypto: { key: ArrayBuffer; - ops: Crypto; - } + ops: ServerCrypto; + }, ): Promise { const msgData = { dcid: dcid.toString(), host: peerHost }; const msgJSON = JSON.stringify(msgData); const msgBuffer = Buffer.from(msgJSON); - const msgSig = Buffer.from( - await crypto.ops.sign(crypto.key, msgBuffer), - ); + const msgSig = Buffer.from(await crypto.ops.sign(crypto.key, msgBuffer)); const tokenData = { msg: msgBuffer.toString('base64url'), sig: msgSig.toString('base64url'), @@ -347,8 +345,8 @@ async function validateToken( peerHost: Host, crypto: { key: ArrayBuffer; - ops: Crypto; - } + ops: ServerCrypto; + }, ): Promise { let tokenData; try { @@ -359,10 +357,7 @@ async function validateToken( if (typeof tokenData !== 'object' || tokenData == null) { return; } - if ( - typeof tokenData.msg !== 'string' || - typeof tokenData.sig !== 'string' - ) { + if (typeof tokenData.msg !== 'string' || typeof tokenData.sig !== 'string') { return; } const msgBuffer = Buffer.from(tokenData.msg, 'base64url'); diff --git a/tests/QUICServer.test.ts b/tests/QUICServer.test.ts index 2b8e3041..814bdaf4 100644 --- a/tests/QUICServer.test.ts +++ b/tests/QUICServer.test.ts @@ -12,7 +12,8 @@ import * as utils from '@/utils'; import * as testsUtils from './utils'; describe(QUICServer.name, () => { - const logger = new Logger(`${QUICServer.name} Test`, LogLevel.WARN, [ new StreamHandler( + const logger = new Logger(`${QUICServer.name} Test`, LogLevel.WARN, [ + new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), ]); @@ -148,7 +149,7 @@ describe(QUICServer.name, () => { logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: '127.0.0.1' as Host + host: '127.0.0.1' as Host, }); expect(quicServer.host).toBe('127.0.0.1'); expect(typeof quicServer.port).toBe('number'); @@ -164,7 +165,7 @@ describe(QUICServer.name, () => { logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: '::1' as Host + host: '::1' as Host, }); expect(quicServer.host).toBe('::1'); expect(typeof quicServer.port).toBe('number'); @@ -180,7 +181,7 @@ describe(QUICServer.name, () => { logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: '::' as Host + host: '::' as Host, }); expect(quicServer.host).toBe('::'); expect(typeof quicServer.port).toBe('number'); @@ -198,13 +199,13 @@ describe(QUICServer.name, () => { logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: '::ffff:127.0.0.1' as Host + host: '::ffff:127.0.0.1' as Host, }); expect(quicServer.host).toBe('::ffff:127.0.0.1'); expect(typeof quicServer.port).toBe('number'); await quicServer.stop(); await quicServer.start({ - host: '::ffff:7f00:1' as Host + host: '::ffff:7f00:1' as Host, }); // Will resolve to dotted-decimal variant expect(quicServer.host).toBe('::ffff:127.0.0.1'); @@ -221,7 +222,7 @@ describe(QUICServer.name, () => { logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: 'localhost' as Hostname + host: 'localhost' as Hostname, }); // Default to using dns lookup, which uses the OS DNS resolver const host = await utils.resolveHostname('localhost' as Hostname); @@ -240,7 +241,7 @@ describe(QUICServer.name, () => { logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: 'abcdef' as Hostname + host: 'abcdef' as Hostname, }); expect(quicServer.host).toBe('127.0.0.1'); expect(typeof quicServer.port).toBe('number'); @@ -253,16 +254,16 @@ describe(QUICServer.name, () => { const quicServer = new QUICServer({ crypto, config: { - // key: keyPairRSAPEM.privateKey, + // Key: keyPairRSAPEM.privateKey, // cert: certRSAPEM, key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, - verifyPeer: false + verifyPeer: false, }, logger: logger.getChild('QUICServer'), }); await quicServer.start({ - host: '127.0.0.1' as Host + host: '127.0.0.1' as Host, }); const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); @@ -278,13 +279,13 @@ describe(QUICServer.name, () => { logger: logger.getChild(QUICSocket.name), }); await socket.start({ - host: '127.0.0.1' as Host + host: '127.0.0.1' as Host, }); // ??? const clientConfig: QUICConfig = { ...clientDefault, - verifyPeer: false + verifyPeer: false, }; // This creates a connection state @@ -297,7 +298,7 @@ describe(QUICServer.name, () => { port: quicServer.port, }, config: clientConfig, - logger: logger.getChild(QUICConnection.name) + logger: logger.getChild(QUICConnection.name), }); connection.addEventListener('error', (e) => { @@ -307,7 +308,7 @@ describe(QUICServer.name, () => { // Trigger the connection await connection.send(); - // wait till it is established + // Wait till it is established console.log('BEFORE ESTABLISHED P'); await connection.establishedP; console.log('AFTER ESTABLISHED P'); @@ -324,7 +325,7 @@ describe(QUICServer.name, () => { console.timeEnd('STOPPED SOCKET'); }); }); - // test('bootstrapping a new connection', async () => { + // Test('bootstrapping a new connection', async () => { // const quicServer = new QUICServer({ // crypto, // config: { diff --git a/tests/config.test.ts b/tests/config.test.ts index 27f1686c..010cc41d 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -65,25 +65,25 @@ describe('config', () => { }); test('build default client config', () => { const config = buildQuicheConfig(clientDefault); - expect(config).toBeDefined() + expect(config).toBeDefined(); }); test('build default server config', () => { const config = buildQuicheConfig(serverDefault); expect(config).toBeDefined(); }); test('build with incorrect configuration', () => { - expect( - () => buildQuicheConfig({ + expect(() => + buildQuicheConfig({ ...serverDefault, - sigalgs: 'ed448' - }) + sigalgs: 'ed448', + }), ).toThrow(errors.ErrorQUICConfig); - expect( - () => buildQuicheConfig({ + expect(() => + buildQuicheConfig({ ...serverDefault, key: [keyPairRSAPEM.privateKey, keyPairECDSAPEM.privateKey], cert: [certRSAPEM], - }) + }), ).toThrow(errors.ErrorQUICConfig); }); test('build with self-signed certificates', () => { @@ -273,13 +273,9 @@ describe('config', () => { key: [ keyPairPEM1.privateKey, keyPairPEM2.privateKey, - keyPairPEM3.privateKey - ], - cert: [ - certPEM1, - certPEM2, - certPEM3 + keyPairPEM3.privateKey, ], + cert: [certPEM1, certPEM2, certPEM3], verifyPeer: true, }); buildQuicheConfig({ @@ -288,13 +284,9 @@ describe('config', () => { key: [ keyPairPEM1.privateKey, keyPairPEM2.privateKey, - keyPairPEM3.privateKey - ], - cert: [ - certPEM1, - certPEM2, - certPEM3 + keyPairPEM3.privateKey, ], + cert: [certPEM1, certPEM2, certPEM3], verifyPeer: true, }); }); diff --git a/tests/native/quiche.connection.lifecycle.test.ts b/tests/native/quiche.connection.lifecycle.test.ts index 066658e8..1d540b0f 100644 --- a/tests/native/quiche.connection.lifecycle.test.ts +++ b/tests/native/quiche.connection.lifecycle.test.ts @@ -133,7 +133,7 @@ describe('quiche connection lifecycle', () => { expect(clientConn.localError()).toEqual({ isApp: false, errorCode: quiche.ConnectionErrorCode.ApplicationError, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(clientConn.timeout()).toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -151,28 +151,19 @@ describe('quiche connection lifecycle', () => { const randomPacket = new Uint8Array(randomPacketBuffer); // Random packets are received after the connection is closed // However they are just dropped automatically - clientConn.recv( - randomPacket, - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(randomPacket, { + to: clientHost, + from: serverHost, + }); // You can receive multiple times without any problems - clientConn.recv( - randomPacket, - { - to: clientHost, - from: serverHost - } - ); - clientConn.recv( - randomPacket, - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(randomPacket, { + to: clientHost, + from: serverHost, + }); + clientConn.recv(randomPacket, { + to: clientHost, + from: serverHost, + }); const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); expect(() => clientConn.send(clientBuffer)).toThrow('Done'); expect(clientConn.isClosed()).toBeTrue(); @@ -200,13 +191,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairRSAPEM.privateKey, cert: certRSAPEM, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -270,13 +261,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairRSAPEM.privateKey, cert: certRSAPEM, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -300,18 +291,19 @@ describe('quiche connection lifecycle', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, + ); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, ); - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const retryDatagramLength = quiche.retry( clientScid, @@ -319,25 +311,22 @@ describe('quiche connection lifecycle', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); expect(dcidOriginal).toEqual(clientDcid); }); @@ -347,18 +336,15 @@ describe('quiche connection lifecycle', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; expect(serverConn.timeout()).toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // Once an idle max timeout is set, this timeout is no longer null // Either the client or server or both can set the idle timeout expect(serverConn.timeout()).not.toBeNull(); @@ -420,13 +406,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairRSAPEM.privateKey, cert: certRSAPEM, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -450,18 +436,19 @@ describe('quiche connection lifecycle', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, + ); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, ); - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const retryDatagramLength = quiche.retry( clientScid, @@ -469,25 +456,22 @@ describe('quiche connection lifecycle', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); expect(dcidOriginal).toEqual(clientDcid); }); @@ -497,41 +481,32 @@ describe('quiche connection lifecycle', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; expect(serverConn.timeout()).toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // Once an idle max timeout is set, this timeout is no longer null // Either the client or server or both can set the idle timeout expect(serverConn.timeout()).not.toBeNull(); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-handshake- server timeout', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); @@ -589,13 +564,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairRSAPEM.privateKey, cert: certRSAPEM, - maxIdleTimeout: 2000 + maxIdleTimeout: 2000, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -619,18 +594,19 @@ describe('quiche connection lifecycle', () => { test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, + ); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, ); - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const retryDatagramLength = quiche.retry( clientScid, @@ -638,25 +614,22 @@ describe('quiche connection lifecycle', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); expect(dcidOriginal).toEqual(clientDcid); }); @@ -666,64 +639,49 @@ describe('quiche connection lifecycle', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); clientDcid = serverScid; serverDcid = clientScid; expect(serverConn.timeout()).toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // Once an idle max timeout is set, this timeout is no longer null // Either the client or server or both can set the idle timeout expect(serverConn.timeout()).not.toBeNull(); }); test('client <-initial- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-handshake- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> sever', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('server is established', async () => { expect(serverConn.isEstablished()).toBeTrue(); @@ -870,12 +828,14 @@ describe('quiche connection lifecycle', () => { // Process the initial frame const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // It will be an initial packet expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); // The SCID is what was generated above - expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual(clientScid); + expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual( + clientScid, + ); // The DCID is randomly generated by the client clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); expect(clientDcid).not.toEqual(clientScid); @@ -886,19 +846,22 @@ describe('quiche connection lifecycle', () => { expect(clientHeaderInitial.versions).toBeNull(); // Version negotiation // The version is supported, we don't need to change - expect(quiche.versionIsSupported(clientHeaderInitial.version)).toBeTrue(); + expect( + quiche.versionIsSupported(clientHeaderInitial.version), + ).toBeTrue(); // Derives a new SCID by signing the client's generated DCID // This is only used during the stateless retry serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const retryDatagramLength = quiche.retry( clientScid, @@ -906,29 +869,26 @@ describe('quiche connection lifecycle', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); const timeoutBeforeRecv = clientConn.timeout(); const serverHeaderRetry = quiche.Header.fromSlice( retryDatagram.subarray(0, retryDatagramLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderRetry.ty).toBe(quiche.Type.Retry); // Retry packet's SCID is the derived SCID expect(new QUICConnectionId(serverHeaderRetry.scid)).toEqual( - serverScid + serverScid, ); expect(new QUICConnectionId(serverHeaderRetry.dcid)).toEqual( - clientScid + clientScid, ); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); const timeoutAfterRecv = clientConn.timeout(); // The timeout is only reset after `recv` is called expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); @@ -943,16 +903,16 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); - expect( - new QUICConnectionId(clientHeaderInitialRetry.scid) - ).toEqual(clientScid); + expect(new QUICConnectionId(clientHeaderInitialRetry.scid)).toEqual( + clientScid, + ); // The DCID is now updated to the server generated one - expect( - new QUICConnectionId(clientHeaderInitialRetry.dcid) - ).toEqual(serverScid); + expect(new QUICConnectionId(clientHeaderInitialRetry.dcid)).toEqual( + serverScid, + ); // The retried initial packet has the signed token expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); @@ -961,7 +921,7 @@ describe('quiche connection lifecycle', () => { const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -972,7 +932,7 @@ describe('quiche connection lifecycle', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); expect(serverConn.timeout()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -991,13 +951,10 @@ describe('quiche connection lifecycle', () => { clientDcid = serverScid; serverDcid = clientScid; // Server receives the retried initial frame - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // The timeout is still null upon the first recv for the server // This is only true because timeout is `0` which is `Infinity` expect(serverConn.timeout()).toBeNull(); @@ -1025,27 +982,28 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isDraining()).toBeFalse(); const serverHeaderInitial = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual(serverScid); - expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual(serverDcid); + expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual( + serverScid, + ); + expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual( + serverDcid, + ); expect(serverHeaderInitial.token).toHaveLength(0); expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); expect(serverHeaderInitial.versions).toBeNull(); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client -initial-> server', async () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); // Timeout is lowered @@ -1057,23 +1015,24 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('client <-handshake- server', async () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderHandshake.ty).toBe(quiche.Type.Handshake); - expect(new QUICConnectionId(serverHeaderHandshake.scid)).toEqual(serverScid); - expect(new QUICConnectionId(serverHeaderHandshake.dcid)).toEqual(serverDcid); + expect(new QUICConnectionId(serverHeaderHandshake.scid)).toEqual( + serverScid, + ); + expect(new QUICConnectionId(serverHeaderHandshake.dcid)).toEqual( + serverDcid, + ); // Timeout is lowered expect(serverConn.timeout()).toBeLessThan(100); expect(serverConn.isTimedOut()).toBeFalse(); @@ -1085,13 +1044,10 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isDraining()).toBeFalse(); expect(() => serverConn.send(serverBuffer)).toThrow('Done'); // Client receives server's handshake frame - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); expect(clientConn.isTimedOut()).toBeFalse(); expect(clientConn.isInEarlyData()).toBeFalse(); expect(clientConn.isEstablished()).toBeTrue(); @@ -1107,19 +1063,16 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderHandshake = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderHandshake.ty).toBe(quiche.Type.Handshake); expect(() => clientConn.send(clientBuffer)).toThrow('Done'); expect(clientConn.timeout()).not.toBeNull(); expect(serverConn.timeout()).not.toBeNull(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(serverConn.timeout()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -1136,21 +1089,18 @@ describe('quiche connection lifecycle', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderShort.ty).toBe(quiche.Type.Short); // SCID is dropped on the short frame expect(serverHeaderShort.scid).toHaveLength(0); expect(new QUICConnectionId(serverHeaderShort.dcid)).toEqual( - clientScid - ); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } + clientScid, ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); // Client connection timeout is now null // Both client and server is established // This is due to max idle timeout of 0 @@ -1163,13 +1113,13 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); // SCID is dropped on the short frame expect(clientHeaderShort.scid).toHaveLength(0); expect(new QUICConnectionId(clientHeaderShort.dcid)).toEqual( - serverScid + serverScid, ); expect(() => clientConn.send(clientBuffer)).toThrow('Done'); expect(clientConn.isTimedOut()).toBeFalse(); @@ -1179,13 +1129,10 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(() => serverConn.send(serverBuffer)).toThrow('Done'); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -1211,7 +1158,7 @@ describe('quiche connection lifecycle', () => { expect(clientConn.localError()).toEqual({ isApp: true, errorCode: 0, - reason: new Uint8Array(Buffer.from('Application Close')) + reason: new Uint8Array(Buffer.from('Application Close')), }); expect(clientConn.timeout()).toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -1224,7 +1171,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); // The timeout begins again @@ -1257,18 +1204,15 @@ describe('quiche connection lifecycle', () => { // Connection is left as draining expect(clientConn.isDraining()).toBeTrue(); // -short-> SERVER - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // The server receives the client's error expect(serverConn.peerError()).toEqual({ isApp: true, errorCode: 0, - reason: new Uint8Array(Buffer.from('Application Close')) + reason: new Uint8Array(Buffer.from('Application Close')), }); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -1412,12 +1356,14 @@ describe('quiche connection lifecycle', () => { // Process the initial frame const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // It will be an initial packet expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); // The SCID is what was generated above - expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual(clientScid); + expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual( + clientScid, + ); // The DCID is randomly generated by the client clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); expect(clientDcid).not.toEqual(clientScid); @@ -1428,19 +1374,22 @@ describe('quiche connection lifecycle', () => { expect(clientHeaderInitial.versions).toBeNull(); // Version negotiation // The version is supported, we don't need to change - expect(quiche.versionIsSupported(clientHeaderInitial.version)).toBeTrue(); + expect( + quiche.versionIsSupported(clientHeaderInitial.version), + ).toBeTrue(); // Derives a new SCID by signing the client's generated DCID // This is only used during the stateless retry serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const retryDatagramLength = quiche.retry( clientScid, @@ -1448,17 +1397,14 @@ describe('quiche connection lifecycle', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); const timeoutBeforeRecv = clientConn.timeout(); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); const timeoutAfterRecv = clientConn.timeout(); // The timeout is only reset after `recv` is called expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); @@ -1473,16 +1419,16 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); - expect( - new QUICConnectionId(clientHeaderInitialRetry.scid) - ).toEqual(clientScid); + expect(new QUICConnectionId(clientHeaderInitialRetry.scid)).toEqual( + clientScid, + ); // The DCID is now updated to the server generated one - expect( - new QUICConnectionId(clientHeaderInitialRetry.dcid) - ).toEqual(serverScid); + expect(new QUICConnectionId(clientHeaderInitialRetry.dcid)).toEqual( + serverScid, + ); // The retried initial packet has the signed token expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); @@ -1491,7 +1437,7 @@ describe('quiche connection lifecycle', () => { const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -1502,7 +1448,7 @@ describe('quiche connection lifecycle', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); expect(serverConn.timeout()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -1517,13 +1463,10 @@ describe('quiche connection lifecycle', () => { // generated DCID, we can update their respective DCID clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // The timeout is still null upon the first recv for the server expect(serverConn.timeout()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -1550,21 +1493,22 @@ describe('quiche connection lifecycle', () => { // At this point the server connection is still not established const serverHeaderInitial = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual(serverScid); - expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual(serverDcid); + expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual( + serverScid, + ); + expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual( + serverDcid, + ); expect(serverHeaderInitial.token).toHaveLength(0); expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); expect(serverHeaderInitial.versions).toBeNull(); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); @@ -1573,7 +1517,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); // Timeout is lowered @@ -1585,13 +1529,10 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('server is established', async () => { expect(serverConn.isEstablished()).toBeTrue(); @@ -1600,16 +1541,13 @@ describe('quiche connection lifecycle', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); // Client connection timeout is now null // Both client and server is established // This is due to max idle timeout of 0 @@ -1622,7 +1560,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); expect(() => clientConn.send(clientBuffer)).toThrow('Done'); @@ -1633,13 +1571,10 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(() => serverConn.send(serverBuffer)).toThrow('Done'); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -1665,7 +1600,7 @@ describe('quiche connection lifecycle', () => { expect(clientConn.localError()).toEqual({ isApp: false, errorCode: 2, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(clientConn.timeout()).toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -1678,7 +1613,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); // The timeout begins again @@ -1711,17 +1646,14 @@ describe('quiche connection lifecycle', () => { // Connection is left as draining expect(clientConn.isDraining()).toBeTrue(); // -short-> SERVER - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(serverConn.peerError()).toEqual({ isApp: false, errorCode: 2, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -1865,12 +1797,14 @@ describe('quiche connection lifecycle', () => { // Process the initial frame const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // It will be an initial packet expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); // The SCID is what was generated above - expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual(clientScid); + expect(new QUICConnectionId(clientHeaderInitial.scid)).toEqual( + clientScid, + ); // The DCID is randomly generated by the client clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); expect(clientDcid).not.toEqual(clientScid); @@ -1881,19 +1815,22 @@ describe('quiche connection lifecycle', () => { expect(clientHeaderInitial.versions).toBeNull(); // Version negotiation // The version is supported, we don't need to change - expect(quiche.versionIsSupported(clientHeaderInitial.version)).toBeTrue(); + expect( + quiche.versionIsSupported(clientHeaderInitial.version), + ).toBeTrue(); // Derives a new SCID by signing the client's generated DCID // This is only used during the stateless retry serverScid = new QUICConnectionId( - await crypto.ops.sign( - crypto.key, - clientDcid, - ), + await crypto.ops.sign(crypto.key, clientDcid), 0, - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); // Stateless retry - const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const token = await utils.mintToken( + clientDcid, + clientHost.host, + crypto, + ); const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); const retryDatagramLength = quiche.retry( clientScid, @@ -1901,17 +1838,14 @@ describe('quiche connection lifecycle', () => { serverScid, token, clientHeaderInitial.version, - retryDatagram + retryDatagram, ); const timeoutBeforeRecv = clientConn.timeout(); // Retry gets sent back to be processed by the client - clientConn.recv( - retryDatagram.subarray(0, retryDatagramLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); const timeoutAfterRecv = clientConn.timeout(); // The timeout is only reset after `recv` is called expect(timeoutAfterRecv).toBeGreaterThan(timeoutBeforeRecv!); @@ -1926,16 +1860,16 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitialRetry.ty).toBe(quiche.Type.Initial); - expect( - new QUICConnectionId(clientHeaderInitialRetry.scid) - ).toEqual(clientScid); + expect(new QUICConnectionId(clientHeaderInitialRetry.scid)).toEqual( + clientScid, + ); // The DCID is now updated to the server generated one - expect( - new QUICConnectionId(clientHeaderInitialRetry.dcid) - ).toEqual(serverScid); + expect(new QUICConnectionId(clientHeaderInitialRetry.dcid)).toEqual( + serverScid, + ); // The retried initial packet has the signed token expect(Buffer.from(clientHeaderInitialRetry.token!)).toEqual(token); expect(clientHeaderInitialRetry.version).toBe(quiche.PROTOCOL_VERSION); @@ -1944,7 +1878,7 @@ describe('quiche connection lifecycle', () => { const dcidOriginal = await utils.validateToken( Buffer.from(clientHeaderInitialRetry.token!), clientHost.host, - crypto + crypto, ); // The original randomly generated DCID was embedded in the token expect(dcidOriginal).toEqual(clientDcid); @@ -1955,7 +1889,7 @@ describe('quiche connection lifecycle', () => { clientDcid, serverHost, clientHost, - serverQuicheConfig + serverQuicheConfig, ); expect(serverConn.timeout()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -1970,13 +1904,10 @@ describe('quiche connection lifecycle', () => { // generated DCID, we can update their respective DCID clientDcid = serverScid; serverDcid = clientScid; - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); // The timeout is still null upon the first recv for the server expect(serverConn.timeout()).toBeNull(); expect(serverConn.isTimedOut()).toBeFalse(); @@ -2003,21 +1934,22 @@ describe('quiche connection lifecycle', () => { // At this point the server connection is still not established const serverHeaderInitial = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderInitial.ty).toBe(quiche.Type.Initial); - expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual(serverScid); - expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual(serverDcid); + expect(new QUICConnectionId(serverHeaderInitial.scid)).toEqual( + serverScid, + ); + expect(new QUICConnectionId(serverHeaderInitial.dcid)).toEqual( + serverDcid, + ); expect(serverHeaderInitial.token).toHaveLength(0); expect(serverHeaderInitial.version).toBe(quiche.PROTOCOL_VERSION); expect(serverHeaderInitial.versions).toBeNull(); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); }); test('client is established', async () => { expect(clientConn.isEstablished()).toBeTrue(); @@ -2026,7 +1958,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderInitial.ty).toBe(quiche.Type.Initial); // Timeout is lowered @@ -2038,13 +1970,10 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); }); test('server is established', async () => { expect(serverConn.isEstablished()).toBeTrue(); @@ -2053,16 +1982,13 @@ describe('quiche connection lifecycle', () => { [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderShort.ty).toBe(quiche.Type.Short); - clientConn.recv( - serverBuffer.subarray(0, serverSendLength), - { - to: clientHost, - from: serverHost - } - ); + clientConn.recv(serverBuffer.subarray(0, serverSendLength), { + to: clientHost, + from: serverHost, + }); // Client connection timeout is now null // Both client and server is established // This is due to max idle timeout of 0 @@ -2075,7 +2001,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); expect(() => clientConn.send(clientBuffer)).toThrow('Done'); @@ -2086,13 +2012,10 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.isClosed()).toBeFalse(); expect(clientConn.isDraining()).toBeFalse(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(() => serverConn.send(serverBuffer)).toThrow('Done'); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); @@ -2118,7 +2041,7 @@ describe('quiche connection lifecycle', () => { expect(clientConn.localError()).toEqual({ isApp: false, errorCode: 1, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(clientConn.timeout()).toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -2131,7 +2054,7 @@ describe('quiche connection lifecycle', () => { [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(clientHeaderShort.ty).toBe(quiche.Type.Short); // The timeout begins again @@ -2163,17 +2086,14 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isClosed()).toBeTrue(); // Connection is left as draining expect(clientConn.isDraining()).toBeTrue(); - serverConn.recv( - clientBuffer.subarray(0, clientSendLength), - { - to: serverHost, - from: clientHost - } - ); + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); expect(serverConn.peerError()).toEqual({ isApp: false, errorCode: 1, - reason: new Uint8Array() + reason: new Uint8Array(), }); expect(serverConn.isTimedOut()).toBeFalse(); expect(serverConn.isInEarlyData()).toBeFalse(); diff --git a/tests/native/quiche.test.ts b/tests/native/quiche.test.ts index ce7ec2de..0f0f0ac0 100644 --- a/tests/native/quiche.test.ts +++ b/tests/native/quiche.test.ts @@ -5,12 +5,9 @@ describe('quiche', () => { test('frame parsing', async () => { let frame: Buffer; frame = Buffer.from('hello world'); - expect(() => quiche.Header.fromSlice( - frame, - quiche.MAX_CONN_ID_LEN) - ).toThrow( - 'BufferTooShort' - ); + expect(() => + quiche.Header.fromSlice(frame, quiche.MAX_CONN_ID_LEN), + ).toThrow('BufferTooShort'); // `InvalidPacket` is also possible but even random bytes can // look like a packet, so it's not tested here }); @@ -25,11 +22,11 @@ describe('quiche', () => { const versionPacketLength = quiche.negotiateVersion( scid, dcid, - versionPacket + versionPacket, ); const serverHeaderVersion = quiche.Header.fromSlice( versionPacket.subarray(0, versionPacketLength), - quiche.MAX_CONN_ID_LEN + quiche.MAX_CONN_ID_LEN, ); expect(serverHeaderVersion.ty).toBe(quiche.Type.VersionNegotiation); }); diff --git a/tests/utils.ts b/tests/utils.ts index 6223a70b..ff4ce934 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -6,7 +6,6 @@ import type QUICServer from '@/QUICServer'; import type QUICStream from '@/QUICStream'; import { Crypto } from '@peculiar/webcrypto'; import * as x509 from '@peculiar/x509'; -import { fc } from '@fast-check/jest'; /** * WebCrypto polyfill from @peculiar/webcrypto From 442814e1967c51d504bc2c269a077b9abae503e2 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 13:16:49 +1000 Subject: [PATCH 05/22] tests: fixing up client tests [ci skip] --- src/errors.ts | 2 +- src/types.ts | 6 - tests/QUICClient.test.ts | 2781 ++++++++++++++++++++------------------ 3 files changed, 1456 insertions(+), 1333 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 6429b49a..75ea9ac0 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,5 @@ import type { POJO } from '@matrixai/errors'; -import { AbstractError } from '@matrixai/errors'; +import AbstractError from '@matrixai/errors'; class ErrorQUIC extends AbstractError { static description = 'QUIC error'; diff --git a/src/types.ts b/src/types.ts index 22e78030..34971a38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,12 +42,6 @@ type ConnectionIdString = Opaque<'ConnectionIdString', string>; * Remember every Node Buffer is an ArrayBuffer */ type ClientCrypto = { - sign(key: ArrayBuffer, data: ArrayBuffer): Promise; - verify( - key: ArrayBuffer, - data: ArrayBuffer, - sig: ArrayBuffer, - ): Promise; randomBytes(data: ArrayBuffer): Promise; }; diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 66a8cd6a..7728c0e1 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -1,4 +1,4 @@ -import type { Crypto, Host, Port } from '@/types'; +import type { ClientCrypto, Host, Port, ServerCrypto } from '@/types'; import type * as events from '@/events'; import type QUICConnection from '@/QUICConnection'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; @@ -14,6 +14,10 @@ import { tlsConfigWithCaArb, tlsConfigWithCaGENOKPArb } from './tlsUtils'; import { sleep } from './utils'; import * as fixtures from './fixtures/certFixtures'; +// TODO: Planed changes... +// 1. convert to a describe each and run tests for each kind of cert. Just for better grouping +// 2. Almost none of the tests need to be fast check, convert to standard tests. + describe(QUICClient.name, () => { const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ new StreamHandler( @@ -21,44 +25,57 @@ describe(QUICClient.name, () => { ), ]); // This has to be setup asynchronously due to key generation - let crypto: { - key: ArrayBuffer; - ops: Crypto; + const serverCrypto: ServerCrypto = { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + }; + let key: ArrayBuffer; + const clientCrypto: ClientCrypto = { + randomBytes: testsUtils.randomBytes, }; let sockets: Set; + let tlsConfigServer : { + key: string, + cert: string, + }; + let tlsConfigClient : { + key: string, + cert: string, + }; + // We need to test the stream making beforeEach(async () => { - crypto = { - key: await testsUtils.generateKey(), - ops: { - sign: testsUtils.sign, - verify: testsUtils.verify, - randomBytes: testsUtils.randomBytes, - }, - }; + key = await testsUtils.generateKeyHMAC(); sockets = new Set(); }); afterEach(async () => { const stopProms: Array> = []; for (const socket of sockets) { - stopProms.push(socket.stop(true)); + stopProms.push(socket.stop({ force: true })); } await Promise.allSettled(stopProms); }); // Are we describing a dual stack client!? describe('dual stack client', () => { - testProp( + test( 'to ipv4 server succeeds', - [tlsConfigWithCaArb], - async (tlsConfigProm) => { + async () => { const connectionEventProm = promise(); - const tlsConfig = await tlsConfigProm; + const keyPair = testsUtils.generateKeyPairRSA(); + const certs = testsUtils.generateCertificate({ + + }) const server = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, logger: logger.getChild(QUICServer.name), config: { - tlsConfig: tlsConfig.tlsConfig, + ...{ + key: tlsConfig.tlsConfig.key + }, verifyPeer: false, }, }); @@ -75,7 +92,9 @@ describe(QUICClient.name, () => { host: '::ffff:127.0.0.1' as Host, port: server.port, localHost: '::' as Host, - crypto, + crypto: { + ops: clientCrypto, + }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, @@ -92,1311 +111,1421 @@ describe(QUICClient.name, () => { }, { numRuns: 10 }, ); - testProp( - 'to ipv6 server succeeds', - [tlsConfigWithCaArb], - async (tlsConfigProm) => { - const connectionEventProm = promise(); - const tlsConfig = await tlsConfigProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '::1' as Host, - port: 0 as Port, - }); - const client = await QUICClient.createQUICClient({ - host: '::1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - expect(conn.localHost).toBe('::1'); - expect(conn.localPort).toBe(server.port); - expect(conn.remoteHost).toBe('::1'); - expect(conn.remotePort).toBe(client.port); - await client.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - testProp( - 'to dual stack server succeeds', - [tlsConfigWithCaArb], - async (tlsConfigProm) => { - const connectionEventProm = promise(); - const tlsConfig = await tlsConfigProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '::' as Host, - port: 0 as Port, - }); - const client = await QUICClient.createQUICClient({ - host: '::' as Host, // Will resolve to ::1 - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - expect(conn.localHost).toBe('::'); - expect(conn.localPort).toBe(server.port); - expect(conn.remoteHost).toBe('::1'); - expect(conn.remotePort).toBe(client.port); - await client.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - }); - test('times out when there is no server', async () => { - // QUICClient repeatedly dials until the connection timeout - await expect( - QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, - port: 56666 as Port, - localHost: '127.0.0.1' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - maxIdleTimeout: 1000, - verifyPeer: false, - }, - }), - ).rejects.toThrow(errors.ErrorQUICConnectionTimeout); - }); - test.todo('client times out after connection stops responding'); - test.todo('server times out after connection stops responding'); - test.todo('server handles socket error'); - test.todo('client handles socket error'); - describe('TLS rotation', () => { - testProp( - 'existing connections config is unchanged and still function', - [tlsConfigWithCaArb, tlsConfigWithCaArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const tlsConfig1 = await tlsConfigProm1; - const tlsConfig2 = await tlsConfigProm2; - fc.pre( - JSON.stringify(tlsConfig1.tlsConfig) !== - JSON.stringify(tlsConfig2.tlsConfig), - ); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig1.tlsConfig, - }, - }); - testsUtils.extractSocket(server, sockets); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client1 = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: true, - verifyPem: tlsConfig1.ca.certChainPem, - }, - }); - testsUtils.extractSocket(client1, sockets); - const peerCertChainInitial = client1.connection.conn.peerCertChain(); - server.updateConfig({ - tlsConfig: tlsConfig2.tlsConfig, - }); - // The existing connection's certs should be unchanged - const peerCertChainNew = client1.connection.conn.peerCertChain(); - expect(peerCertChainNew![0].toString()).toStrictEqual( - peerCertChainInitial![0].toString(), - ); - await client1.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - testProp( - 'new connections use new config', - [tlsConfigWithCaGENOKPArb, tlsConfigWithCaGENOKPArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const tlsConfig1 = await tlsConfigProm1; - const tlsConfig2 = await tlsConfigProm2; - fc.pre( - JSON.stringify(tlsConfig1.tlsConfig) !== - JSON.stringify(tlsConfig2.tlsConfig), - ); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig1.tlsConfig, - }, - }); - testsUtils.extractSocket(server, sockets); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client1 = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPem: tlsConfig1.ca.certChainPem, - }, - }); - testsUtils.extractSocket(client1, sockets); - const peerCertChainInitial = client1.connection.conn.peerCertChain(); - server.updateConfig({ - tlsConfig: tlsConfig2.tlsConfig, - }); - // Starting a new connection has a different peerCertChain - const client2 = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: true, - verifyPem: tlsConfig2.ca.certChainPem, - }, - }); - testsUtils.extractSocket(client2, sockets); - const peerCertChainNew = client2.connection.conn.peerCertChain(); - expect(peerCertChainNew![0].toString()).not.toStrictEqual( - peerCertChainInitial![0].toString(), - ); - await client1.destroy(); - await client2.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - }); - describe('graceful tls handshake', () => { - testProp( - 'server verification succeeds', - [tlsConfigWithCaArb], - async (tlsConfigsProm) => { - const tlsConfigs = await tlsConfigsProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfigs.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - const handleConnectionEventProm = promise(); - server.addEventListener( - 'connection', - handleConnectionEventProm.resolveP, - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - // Connection should succeed - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: true, - verifyPem: tlsConfigs.ca.certChainPem, - }, - }); - testsUtils.extractSocket(client, sockets); - await handleConnectionEventProm.p; - await client.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - // Fixme: client verification works regardless of certs - testProp.skip( - 'client verification succeeds', - [tlsConfigWithCaArb, tlsConfigWithCaArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const tlsConfigs1 = await tlsConfigProm1; - const tlsConfigs2 = await tlsConfigProm2; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfigs1.tlsConfig, - verifyPem: tlsConfigs2.ca.certChainPem, - verifyPeer: true, - }, - }); - const handleConnectionEventProm = promise(); - server.addEventListener( - 'connection', - handleConnectionEventProm.resolveP, - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - // Connection should succeed - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - tlsConfig: tlsConfigs2.tlsConfig, - verifyPeer: false, - }, - }); - await client.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - testProp( - 'client and server verification succeeds', - [tlsConfigWithCaArb, tlsConfigWithCaArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const tlsConfigs1 = await tlsConfigProm1; - const tlsConfigs2 = await tlsConfigProm2; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfigs1.tlsConfig, - verifyPem: tlsConfigs2.ca.certChainPem, - verifyPeer: true, - }, - }); - testsUtils.extractSocket(server, sockets); - const handleConnectionEventProm = promise(); - server.addEventListener( - 'connection', - handleConnectionEventProm.resolveP, - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - // Connection should succeed - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - tlsConfig: tlsConfigs2.tlsConfig, - verifyPem: tlsConfigs1.ca.certChainPem, - verifyPeer: true, - }, - }); - testsUtils.extractSocket(client, sockets); - await handleConnectionEventProm.p; - await client.destroy(); - await server.stop(); - }, - { numRuns: 10 }, - ); - testProp( - 'graceful failure verifying server', - [tlsConfigWithCaArb], - async (tlsConfigsProm) => { - const tlsConfigs1 = await tlsConfigsProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfigs1.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - const handleConnectionEventProm = promise(); - server.addEventListener( - 'connection', - handleConnectionEventProm.resolveP, - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - // Connection should succeed - await expect( - QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: true, - }, - }), - ).toReject(); - await handleConnectionEventProm.p; - // Expect connection on the server to have ended - // @ts-ignore: kidnap protected property - // const connectionMap = server.connectionMap; - // Expect(connectionMap.serverConnections.size).toBe(0); - await server.stop(); - }, - { numRuns: 3 }, - ); - // Fixme: client verification works regardless of certs - testProp.skip( - 'graceful failure verifying client', - [tlsConfigWithCaArb, tlsConfigWithCaArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const tlsConfigs1 = await tlsConfigProm1; - const tlsConfigs2 = await tlsConfigProm2; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfigs1.tlsConfig, - verifyPeer: true, - }, - }); - testsUtils.extractSocket(server, sockets); - const handleConnectionEventProm = promise(); - server.addEventListener( - 'connection', - handleConnectionEventProm.resolveP, - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - // Connection should succeed - await expect( - QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - tlsConfig: tlsConfigs2.tlsConfig, - verifyPeer: false, - }, - }), - ).toReject(); - await handleConnectionEventProm.p; - // Expect connection on the server to have ended - // @ts-ignore: kidnap protected property - const connectionMap = server.connectionMap; - expect(connectionMap.serverConnections.size).toBe(0); - await server.stop(); - }, - { numRuns: 3 }, - ); - testProp( - 'graceful failure verifying client and server', - [tlsConfigWithCaArb, tlsConfigWithCaArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const tlsConfigs1 = await tlsConfigProm1; - const tlsConfigs2 = await tlsConfigProm2; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfigs1.tlsConfig, - verifyPeer: true, - }, - }); - testsUtils.extractSocket(server, sockets); - const handleConnectionEventProm = promise(); - server.addEventListener( - 'connection', - handleConnectionEventProm.resolveP, - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - // Connection should succeed - await expect( - QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - tlsConfig: tlsConfigs2.tlsConfig, - verifyPeer: true, - }, - }), - ).toReject(); - await handleConnectionEventProm.p; - // Expect connection on the server to have ended - // @ts-ignore: kidnap protected property - // const connectionMap = server.connectionMap; - // Expect(connectionMap.serverConnections.size).toBe(0); - await server.stop(); - }, - { numRuns: 3 }, - ); - }); - describe('UDP nat punching', () => { - test('server can send init packets', async () => { - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - }); - await server.start({ - host: '127.0.0.1' as Host, - }); - testsUtils.extractSocket(server, sockets); - // @ts-ignore: kidnap protected property - const socket = server.socket; - const mockedSend = jest.spyOn(socket, 'send'); - // The server can send packets - // Should send 4 packets in 2 seconds - const result = await server.initHolePunch( - { - host: '127.0.0.1' as Host, - port: 52222 as Port, - }, - 2000, - ); - expect(mockedSend).toHaveBeenCalledTimes(4); - expect(result).toBeFalse(); - await server.stop(); - }); - test('init ends when connection establishes', async () => { - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - await server.start({ - host: '127.0.0.1' as Host, - }); - // The server can send packets - // Should send 4 packets in 2 seconds - const clientProm = sleep(1000) - .then(async () => { - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - localPort: 55556 as Port, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - await client.destroy({ force: true }); - }) - .catch(() => {}); - const result = await server.initHolePunch( - { - host: '127.0.0.1' as Host, - port: 55556 as Port, - }, - 2000, - ); - await clientProm; - expect(result).toBeTrue(); - await server.stop(); - }); - test('init returns with existing connections', async () => { - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - localPort: 55556 as Port, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const result = await Promise.race([ - server.initHolePunch( - { - host: '127.0.0.1' as Host, - port: 55556 as Port, - }, - 2000, - ), - sleep(10).then(() => { - throw Error('timed out'); - }), - ]); - expect(result).toBeTrue(); - await client.destroy({ force: true }); - await server.stop(); - }); - }); - describe('handles random packets', () => { - testProp( - 'client handles random noise from server', - [ - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - ], - async (data, messages) => { - const socket = new QUICSocket({ - crypto, - logger: logger.getChild('socket'), - }); - await socket.start({ - host: '127.0.0.1' as Host, - }); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - socket, - }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const serverStreamProms: Array> = []; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - serverStreamProms.push(streamProm); - }, - ); - // Sending random data to client from the perspective of the server - let running = true; - const randomDataProm = (async () => { - let count = 0; - while (running) { - await socket.send( - data[count % data.length], - client.port, - '127.0.0.1', - ); - await sleep(5); - count += 1; - } - })(); - // We want to check that things function fine between bad data - const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); - await Promise.all([ - (async () => { - // Write data - const writer = stream.writable.getWriter(); - for (const message of messages) { - await writer.write(message); - await sleep(7); - } - await writer.close(); - })(), - (async () => { - // Consume readable - for await (const _ of stream.readable) { - // Do nothing - } - })(), - ]); - running = false; - })(); - // Wait for running activity to finish, should complete without error - await Promise.all([ - randomActivityProm, - serverStreamProms, - randomDataProm, - ]); - await client.destroy({ force: true }); - await server.stop(); - await socket.stop(); - }, - { numRuns: 1 }, - ); - testProp( - 'client handles random noise from external', - [ - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - ], - async (data, messages) => { - const socket = new QUICSocket({ - crypto, - logger: logger.getChild('socket'), - }); - await socket.start({ - host: '127.0.0.1' as Host, - }); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const serverStreamProms: Array> = []; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - serverStreamProms.push(streamProm); - }, - ); - // Sending random data to client from the perspective of the server - let running = true; - const randomDataProm = (async () => { - let count = 0; - while (running) { - await socket.send( - data[count % data.length], - client.port, - '127.0.0.1', - ); - await sleep(5); - count += 1; - } - })(); - // We want to check that things function fine between bad data - const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); - await Promise.all([ - (async () => { - // Write data - const writer = stream.writable.getWriter(); - for (const message of messages) { - await writer.write(message); - await sleep(7); - } - await writer.close(); - })(), - (async () => { - // Consume readable - for await (const _ of stream.readable) { - // Do nothing - } - })(), - ]); - running = false; - })(); - // Wait for running activity to finish, should complete without error - await Promise.all([ - randomActivityProm, - serverStreamProms, - randomDataProm, - ]); - await client.destroy({ force: true }); - await server.stop(); - await socket.stop(); - }, - { numRuns: 1 }, - ); - testProp( - 'server handles random noise from client', - [ - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - ], - async (data, messages) => { - const socket = new QUICSocket({ - crypto, - logger: logger.getChild('socket'), - }); - await socket.start({ - host: '127.0.0.1' as Host, - }); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, - port: server.port, - socket, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const serverStreamProms: Array> = []; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - serverStreamProms.push(streamProm); - }, - ); - // Sending random data to client from the perspective of the server - let running = true; - const randomDataProm = (async () => { - let count = 0; - while (running) { - await socket.send( - data[count % data.length], - server.port, - '127.0.0.1', - ); - await sleep(5); - count += 1; - } - })(); - // We want to check that things function fine between bad data - const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); - await Promise.all([ - (async () => { - // Write data - const writer = stream.writable.getWriter(); - for (const message of messages) { - await writer.write(message); - await sleep(7); - } - await writer.close(); - })(), - (async () => { - // Consume readable - for await (const _ of stream.readable) { - // Do nothing - } - })(), - ]); - running = false; - })(); - // Wait for running activity to finish, should complete without error - await Promise.all([ - randomActivityProm, - serverStreamProms, - randomDataProm, - ]); - await client.destroy({ force: true }); - await server.stop(); - await socket.stop(); - }, - { numRuns: 1 }, - ); - testProp( - 'server handles random noise from external', - [ - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - ], - async (data, messages) => { - const socket = new QUICSocket({ - crypto, - logger: logger.getChild('socket'), - }); - await socket.start({ - host: '127.0.0.1' as Host, - }); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: fixtures.tlsConfigMemRSA1, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - const connectionEventProm = promise(); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, - port: server.port, - localHost: '127.0.0.1' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const serverStreamProms: Array> = []; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - serverStreamProms.push(streamProm); - }, - ); - // Sending random data to client from the perspective of the server - let running = true; - const randomDataProm = (async () => { - let count = 0; - while (running) { - await socket.send( - data[count % data.length], - server.port, - '127.0.0.1', - ); - await sleep(5); - count += 1; - } - })(); - // We want to check that things function fine between bad data - const randomActivityProm = (async () => { - const stream = await client.connection.streamNew(); - await Promise.all([ - (async () => { - // Write data - const writer = stream.writable.getWriter(); - for (const message of messages) { - await writer.write(message); - await sleep(7); - } - await writer.close(); - })(), - (async () => { - // Consume readable - for await (const _ of stream.readable) { - // Do nothing - } - })(), - ]); - running = false; - })(); - // Wait for running activity to finish, should complete without error - await Promise.all([ - randomActivityProm, - serverStreamProms, - randomDataProm, - ]); - await client.destroy({ force: true }); - await server.stop(); - await socket.stop(); - }, - { numRuns: 1 }, - ); - }); - describe('keepalive', () => { - const tlsConfig = fixtures.tlsConfigMemRSA1; - test('connection can time out on client', async () => { - const connectionEventProm = promise(); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig, - verifyPeer: false, - maxIdleTimeout: 1000, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e.detail), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - maxIdleTimeout: 100, - }, - }); - testsUtils.extractSocket(client, sockets); - // Setting no keepalive should cause the connection to time out - // It has cleaned up due to timeout - const clientConnection = client.connection; - const clientTimeoutProm = promise(); - clientConnection.addEventListener( - 'error', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - clientTimeoutProm.resolveP(); - } - }, - ); - await clientTimeoutProm.p; - const serverConnection = await connectionEventProm.p; - await sleep(100); - // Server and client has cleaned up - expect(clientConnection[destroyed]).toBeTrue(); - expect(serverConnection[destroyed]).toBeTrue(); - - await client.destroy(); - await server.stop(); - }); - test('connection can time out on server', async () => { - const connectionEventProm = promise(); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig, - verifyPeer: false, - maxIdleTimeout: 100, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e.detail), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - maxIdleTimeout: 1000, - }, - }); - testsUtils.extractSocket(client, sockets); - // Setting no keepalive should cause the connection to time out - // It has cleaned up due to timeout - const clientConnection = client.connection; - const serverConnection = await connectionEventProm.p; - const serverTimeoutProm = promise(); - serverConnection.addEventListener( - 'error', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - serverTimeoutProm.resolveP(); - } - }, - ); - await serverTimeoutProm.p; - await sleep(100); - // Server and client has cleaned up - expect(clientConnection[destroyed]).toBeTrue(); - expect(serverConnection[destroyed]).toBeTrue(); - - await client.destroy(); - await server.stop(); - }); - test('keep alive prevents timeout on client', async () => { - const connectionEventProm = promise(); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig, - verifyPeer: false, - maxIdleTimeout: 20000, - logKeys: './tmp/key1.log', - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e.detail), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - maxIdleTimeout: 100, - }, - keepaliveIntervalTime: 50, - }); - testsUtils.extractSocket(client, sockets); - // Setting no keepalive should cause the connection to time out - // It has cleaned up due to timeout - const clientConnection = client.connection; - const clientTimeoutProm = promise(); - clientConnection.addEventListener( - 'error', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - clientTimeoutProm.resolveP(); - } - }, - ); - await connectionEventProm.p; - // Connection would timeout after 100ms if keep alive didn't work - await Promise.race([ - sleep(300), - clientTimeoutProm.p.then(() => { - throw Error('Connection timed out'); - }), - ]); - await client.destroy(); - await server.stop(); - }); - test('keep alive prevents timeout on server', async () => { - const connectionEventProm = promise(); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig, - verifyPeer: false, - maxIdleTimeout: 100, - logKeys: './tmp/key1.log', - }, - keepaliveIntervalTime: 50, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e.detail), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - maxIdleTimeout: 20000, - }, - }); - testsUtils.extractSocket(client, sockets); - // Setting no keepalive should cause the connection to time out - // It has cleaned up due to timeout - const serverConnection = await connectionEventProm.p; - const serverTimeoutProm = promise(); - serverConnection.addEventListener( - 'error', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - serverTimeoutProm.resolveP(); - } - }, - ); - // Connection would time out after 100ms if keep alive didn't work - await Promise.race([ - sleep(300), - serverTimeoutProm.p.then(() => { - throw Error('Connection timed out'); - }), - ]); - await client.destroy(); - await server.stop(); - }); - test('client keep alive prevents timeout on server', async () => { - const connectionEventProm = promise(); - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig, - verifyPeer: false, - maxIdleTimeout: 100, - logKeys: './tmp/key1.log', - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e.detail), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - maxIdleTimeout: 20000, - }, - keepaliveIntervalTime: 50, - }); - testsUtils.extractSocket(client, sockets); - // Setting no keepalive should cause the connection to time out - // It has cleaned up due to timeout - const serverConnection = await connectionEventProm.p; - const serverTimeoutProm = promise(); - serverConnection.addEventListener( - 'error', - (event: events.QUICConnectionErrorEvent) => { - if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - serverTimeoutProm.resolveP(); - } - }, - ); - // Connection would time out after 100ms if keep alive didn't work - await Promise.race([ - sleep(300), - serverTimeoutProm.p.then(() => { - throw Error('Connection timed out'); - }), - ]); - await client.destroy(); - await server.stop(); - }); - test('Keep alive does not prevent connection timeout', async () => { - const clientProm = QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: 54444 as Port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - maxIdleTimeout: 100, - }, - keepaliveIntervalTime: 50, - }); - await expect(clientProm).rejects.toThrow( - errors.ErrorQUICConnectionTimeout, - ); - }); + // testProp( + // 'to ipv6 server succeeds', + // [tlsConfigWithCaArb], + // async (tlsConfigProm) => { + // const connectionEventProm = promise(); + // const tlsConfig = await tlsConfigProm; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // ...tlsConfigServer, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e), + // ); + // await server.start({ + // host: '::1' as Host, + // port: 0 as Port, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const conn = (await connectionEventProm.p).detail; + // expect(conn.localHost).toBe('::1'); + // expect(conn.localPort).toBe(server.port); + // expect(conn.remoteHost).toBe('::1'); + // expect(conn.remotePort).toBe(client.port); + // await client.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); + // testProp( + // 'to dual stack server succeeds', + // [tlsConfigWithCaArb], + // async (tlsConfigProm) => { + // const connectionEventProm = promise(); + // const tlsConfig = await tlsConfigProm; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // ...tlsConfigServer, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e), + // ); + // await server.start({ + // host: '::' as Host, + // port: 0 as Port, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::' as Host, // Will resolve to ::1 + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const conn = (await connectionEventProm.p).detail; + // expect(conn.localHost).toBe('::'); + // expect(conn.localPort).toBe(server.port); + // expect(conn.remoteHost).toBe('::1'); + // expect(conn.remotePort).toBe(client.port); + // await client.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); }); + // test('times out when there is no server', async () => { + // // QUICClient repeatedly dials until the connection timeout + // await expect( + // QUICClient.createQUICClient({ + // host: '127.0.0.1' as Host, + // port: 56666 as Port, + // localHost: '127.0.0.1' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // maxIdleTimeout: 1000, + // verifyPeer: false, + // }, + // }), + // ).rejects.toThrow(errors.ErrorQUICConnectionTimeout); + // }); + // test.todo('client times out after connection stops responding'); + // test.todo('server times out after connection stops responding'); + // test.todo('server handles socket error'); + // test.todo('client handles socket error'); + // describe('TLS rotation', () => { + // testProp( + // 'existing connections config is unchanged and still function', + // [tlsConfigWithCaArb, tlsConfigWithCaArb], + // async (tlsConfigProm1, tlsConfigProm2) => { + // const tlsConfig1 = await tlsConfigProm1; + // const tlsConfig2 = await tlsConfigProm2; + // fc.pre( + // JSON.stringify(tlsConfig1.tlsConfig) !== + // JSON.stringify(tlsConfig2.tlsConfig), + // ); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // ...tlsConfigServer, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client1 = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: true, + // verifyPem: tlsConfig1.ca.certChainPem, + // }, + // }); + // testsUtils.extractSocket(client1, sockets); + // const peerCertChainInitial = client1.connection.conn.peerCertChain(); + // server.updateConfig({ + // tlsConfig: tlsConfig2.tlsConfig, + // }); + // // The existing connection's certs should be unchanged + // const peerCertChainNew = client1.connection.conn.peerCertChain(); + // expect(peerCertChainNew![0].toString()).toStrictEqual( + // peerCertChainInitial![0].toString(), + // ); + // await client1.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); + // testProp( + // 'new connections use new config', + // [tlsConfigWithCaGENOKPArb, tlsConfigWithCaGENOKPArb], + // async (tlsConfigProm1, tlsConfigProm2) => { + // const tlsConfig1 = await tlsConfigProm1; + // const tlsConfig2 = await tlsConfigProm2; + // fc.pre( + // JSON.stringify(tlsConfig1.tlsConfig) !== + // JSON.stringify(tlsConfig2.tlsConfig), + // ); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfig1.tlsConfig, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client1 = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPem: tlsConfig1.ca.certChainPem, + // }, + // }); + // testsUtils.extractSocket(client1, sockets); + // const peerCertChainInitial = client1.connection.conn.peerCertChain(); + // server.updateConfig({ + // tlsConfig: tlsConfig2.tlsConfig, + // }); + // // Starting a new connection has a different peerCertChain + // const client2 = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: true, + // verifyPem: tlsConfig2.ca.certChainPem, + // }, + // }); + // testsUtils.extractSocket(client2, sockets); + // const peerCertChainNew = client2.connection.conn.peerCertChain(); + // expect(peerCertChainNew![0].toString()).not.toStrictEqual( + // peerCertChainInitial![0].toString(), + // ); + // await client1.destroy(); + // await client2.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); + // }); + // describe('graceful tls handshake', () => { + // testProp( + // 'server verification succeeds', + // [tlsConfigWithCaArb], + // async (tlsConfigsProm) => { + // const tlsConfigs = await tlsConfigsProm; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfigs.tlsConfig, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const handleConnectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // handleConnectionEventProm.resolveP, + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // Connection should succeed + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: true, + // verifyPem: tlsConfigs.ca.certChainPem, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // await handleConnectionEventProm.p; + // await client.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); + // // Fixme: client verification works regardless of certs + // testProp.skip( + // 'client verification succeeds', + // [tlsConfigWithCaArb, tlsConfigWithCaArb], + // async (tlsConfigProm1, tlsConfigProm2) => { + // const tlsConfigs1 = await tlsConfigProm1; + // const tlsConfigs2 = await tlsConfigProm2; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfigs1.tlsConfig, + // verifyPem: tlsConfigs2.ca.certChainPem, + // verifyPeer: true, + // }, + // }); + // const handleConnectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // handleConnectionEventProm.resolveP, + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // Connection should succeed + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // tlsConfig: tlsConfigs2.tlsConfig, + // verifyPeer: false, + // }, + // }); + // await client.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); + // testProp( + // 'client and server verification succeeds', + // [tlsConfigWithCaArb, tlsConfigWithCaArb], + // async (tlsConfigProm1, tlsConfigProm2) => { + // const tlsConfigs1 = await tlsConfigProm1; + // const tlsConfigs2 = await tlsConfigProm2; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfigs1.tlsConfig, + // verifyPem: tlsConfigs2.ca.certChainPem, + // verifyPeer: true, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const handleConnectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // handleConnectionEventProm.resolveP, + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // Connection should succeed + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // tlsConfig: tlsConfigs2.tlsConfig, + // verifyPem: tlsConfigs1.ca.certChainPem, + // verifyPeer: true, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // await handleConnectionEventProm.p; + // await client.destroy(); + // await server.stop(); + // }, + // { numRuns: 10 }, + // ); + // testProp( + // 'graceful failure verifying server', + // [tlsConfigWithCaArb], + // async (tlsConfigsProm) => { + // const tlsConfigs1 = await tlsConfigsProm; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfigs1.tlsConfig, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const handleConnectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // handleConnectionEventProm.resolveP, + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // Connection should succeed + // await expect( + // QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: true, + // }, + // }), + // ).toReject(); + // await handleConnectionEventProm.p; + // // Expect connection on the server to have ended + // // @ts-ignore: kidnap protected property + // // const connectionMap = server.connectionMap; + // // Expect(connectionMap.serverConnections.size).toBe(0); + // await server.stop(); + // }, + // { numRuns: 3 }, + // ); + // // Fixme: client verification works regardless of certs + // testProp.skip( + // 'graceful failure verifying client', + // [tlsConfigWithCaArb, tlsConfigWithCaArb], + // async (tlsConfigProm1, tlsConfigProm2) => { + // const tlsConfigs1 = await tlsConfigProm1; + // const tlsConfigs2 = await tlsConfigProm2; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfigs1.tlsConfig, + // verifyPeer: true, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const handleConnectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // handleConnectionEventProm.resolveP, + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // Connection should succeed + // await expect( + // QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // tlsConfig: tlsConfigs2.tlsConfig, + // verifyPeer: false, + // }, + // }), + // ).toReject(); + // await handleConnectionEventProm.p; + // // Expect connection on the server to have ended + // // @ts-ignore: kidnap protected property + // const connectionMap = server.connectionMap; + // expect(connectionMap.serverConnections.size).toBe(0); + // await server.stop(); + // }, + // { numRuns: 3 }, + // ); + // testProp( + // 'graceful failure verifying client and server', + // [tlsConfigWithCaArb, tlsConfigWithCaArb], + // async (tlsConfigProm1, tlsConfigProm2) => { + // const tlsConfigs1 = await tlsConfigProm1; + // const tlsConfigs2 = await tlsConfigProm2; + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: tlsConfigs1.tlsConfig, + // verifyPeer: true, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const handleConnectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // handleConnectionEventProm.resolveP, + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // Connection should succeed + // await expect( + // QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // tlsConfig: tlsConfigs2.tlsConfig, + // verifyPeer: true, + // }, + // }), + // ).toReject(); + // await handleConnectionEventProm.p; + // // Expect connection on the server to have ended + // // @ts-ignore: kidnap protected property + // // const connectionMap = server.connectionMap; + // // Expect(connectionMap.serverConnections.size).toBe(0); + // await server.stop(); + // }, + // { numRuns: 3 }, + // ); + // }); + // describe('UDP nat punching', () => { + // test('server can send init packets', async () => { + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // }); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // testsUtils.extractSocket(server, sockets); + // // @ts-ignore: kidnap protected property + // const socket = server.socket; + // const mockedSend = jest.spyOn(socket, 'send'); + // // The server can send packets + // // Should send 4 packets in 2 seconds + // const result = await server.initHolePunch( + // { + // host: '127.0.0.1' as Host, + // port: 52222 as Port, + // }, + // 2000, + // ); + // expect(mockedSend).toHaveBeenCalledTimes(4); + // expect(result).toBeFalse(); + // await server.stop(); + // }); + // test('init ends when connection establishes', async () => { + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // // The server can send packets + // // Should send 4 packets in 2 seconds + // const clientProm = sleep(1000) + // .then(async () => { + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // localPort: 55556 as Port, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // await client.destroy({ force: true }); + // }) + // .catch(() => {}); + // const result = await server.initHolePunch( + // { + // host: '127.0.0.1' as Host, + // port: 55556 as Port, + // }, + // 2000, + // ); + // await clientProm; + // expect(result).toBeTrue(); + // await server.stop(); + // }); + // test('init returns with existing connections', async () => { + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // localPort: 55556 as Port, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const result = await Promise.race([ + // server.initHolePunch( + // { + // host: '127.0.0.1' as Host, + // port: 55556 as Port, + // }, + // 2000, + // ), + // sleep(10).then(() => { + // throw Error('timed out'); + // }), + // ]); + // expect(result).toBeTrue(); + // await client.destroy({ force: true }); + // await server.stop(); + // }); + // }); + // describe('handles random packets', () => { + // testProp( + // 'client handles random noise from server', + // [ + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // ], + // async (data, messages) => { + // const socket = new QUICSocket({ + // logger: logger.getChild('socket'), + // }); + // await socket.start({ + // host: '127.0.0.1' as Host, + // }); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // socket, + // }); + // testsUtils.extractSocket(server, sockets); + // const connectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const conn = (await connectionEventProm.p).detail; + // // Do the test + // const serverStreamProms: Array> = []; + // conn.addEventListener( + // 'stream', + // (streamEvent: events.QUICConnectionStreamEvent) => { + // const stream = streamEvent.detail; + // const streamProm = stream.readable.pipeTo(stream.writable); + // serverStreamProms.push(streamProm); + // }, + // ); + // // Sending random data to client from the perspective of the server + // let running = true; + // const randomDataProm = (async () => { + // let count = 0; + // while (running) { + // await socket.send( + // data[count % data.length], + // client.port, + // '127.0.0.1', + // ); + // await sleep(5); + // count += 1; + // } + // })(); + // // We want to check that things function fine between bad data + // const randomActivityProm = (async () => { + // const stream = await client.connection.streamNew(); + // await Promise.all([ + // (async () => { + // // Write data + // const writer = stream.writable.getWriter(); + // for (const message of messages) { + // await writer.write(message); + // await sleep(7); + // } + // await writer.close(); + // })(), + // (async () => { + // // Consume readable + // for await (const _ of stream.readable) { + // // Do nothing + // } + // })(), + // ]); + // running = false; + // })(); + // // Wait for running activity to finish, should complete without error + // await Promise.all([ + // randomActivityProm, + // serverStreamProms, + // randomDataProm, + // ]); + // await client.destroy({ force: true }); + // await server.stop(); + // await socket.stop(); + // }, + // { numRuns: 1 }, + // ); + // testProp( + // 'client handles random noise from external', + // [ + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // ], + // async (data, messages) => { + // const socket = new QUICSocket({ + // logger: logger.getChild('socket'), + // }); + // await socket.start({ + // host: '127.0.0.1' as Host, + // }); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const connectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const conn = (await connectionEventProm.p).detail; + // // Do the test + // const serverStreamProms: Array> = []; + // conn.addEventListener( + // 'stream', + // (streamEvent: events.QUICConnectionStreamEvent) => { + // const stream = streamEvent.detail; + // const streamProm = stream.readable.pipeTo(stream.writable); + // serverStreamProms.push(streamProm); + // }, + // ); + // // Sending random data to client from the perspective of the server + // let running = true; + // const randomDataProm = (async () => { + // let count = 0; + // while (running) { + // await socket.send( + // data[count % data.length], + // client.port, + // '127.0.0.1', + // ); + // await sleep(5); + // count += 1; + // } + // })(); + // // We want to check that things function fine between bad data + // const randomActivityProm = (async () => { + // const stream = await client.connection.streamNew(); + // await Promise.all([ + // (async () => { + // // Write data + // const writer = stream.writable.getWriter(); + // for (const message of messages) { + // await writer.write(message); + // await sleep(7); + // } + // await writer.close(); + // })(), + // (async () => { + // // Consume readable + // for await (const _ of stream.readable) { + // // Do nothing + // } + // })(), + // ]); + // running = false; + // })(); + // // Wait for running activity to finish, should complete without error + // await Promise.all([ + // randomActivityProm, + // serverStreamProms, + // randomDataProm, + // ]); + // await client.destroy({ force: true }); + // await server.stop(); + // await socket.stop(); + // }, + // { numRuns: 1 }, + // ); + // testProp( + // 'server handles random noise from client', + // [ + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // ], + // async (data, messages) => { + // const socket = new QUICSocket({ + // logger: logger.getChild('socket'), + // }); + // await socket.start({ + // host: '127.0.0.1' as Host, + // }); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const connectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '127.0.0.1' as Host, + // port: server.port, + // socket, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const conn = (await connectionEventProm.p).detail; + // // Do the test + // const serverStreamProms: Array> = []; + // conn.addEventListener( + // 'stream', + // (streamEvent: events.QUICConnectionStreamEvent) => { + // const stream = streamEvent.detail; + // const streamProm = stream.readable.pipeTo(stream.writable); + // serverStreamProms.push(streamProm); + // }, + // ); + // // Sending random data to client from the perspective of the server + // let running = true; + // const randomDataProm = (async () => { + // let count = 0; + // while (running) { + // await socket.send( + // data[count % data.length], + // server.port, + // '127.0.0.1', + // ); + // await sleep(5); + // count += 1; + // } + // })(); + // // We want to check that things function fine between bad data + // const randomActivityProm = (async () => { + // const stream = await client.connection.streamNew(); + // await Promise.all([ + // (async () => { + // // Write data + // const writer = stream.writable.getWriter(); + // for (const message of messages) { + // await writer.write(message); + // await sleep(7); + // } + // await writer.close(); + // })(), + // (async () => { + // // Consume readable + // for await (const _ of stream.readable) { + // // Do nothing + // } + // })(), + // ]); + // running = false; + // })(); + // // Wait for running activity to finish, should complete without error + // await Promise.all([ + // randomActivityProm, + // serverStreamProms, + // randomDataProm, + // ]); + // await client.destroy({ force: true }); + // await server.stop(); + // await socket.stop(); + // }, + // { numRuns: 1 }, + // ); + // testProp( + // 'server handles random noise from external', + // [ + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + // ], + // async (data, messages) => { + // const socket = new QUICSocket({ + // logger: logger.getChild('socket'), + // }); + // await socket.start({ + // host: '127.0.0.1' as Host, + // }); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig: fixtures.tlsConfigMemRSA1, + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // const connectionEventProm = promise(); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '127.0.0.1' as Host, + // port: server.port, + // localHost: '127.0.0.1' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // const conn = (await connectionEventProm.p).detail; + // // Do the test + // const serverStreamProms: Array> = []; + // conn.addEventListener( + // 'stream', + // (streamEvent: events.QUICConnectionStreamEvent) => { + // const stream = streamEvent.detail; + // const streamProm = stream.readable.pipeTo(stream.writable); + // serverStreamProms.push(streamProm); + // }, + // ); + // // Sending random data to client from the perspective of the server + // let running = true; + // const randomDataProm = (async () => { + // let count = 0; + // while (running) { + // await socket.send( + // data[count % data.length], + // server.port, + // '127.0.0.1', + // ); + // await sleep(5); + // count += 1; + // } + // })(); + // // We want to check that things function fine between bad data + // const randomActivityProm = (async () => { + // const stream = await client.connection.streamNew(); + // await Promise.all([ + // (async () => { + // // Write data + // const writer = stream.writable.getWriter(); + // for (const message of messages) { + // await writer.write(message); + // await sleep(7); + // } + // await writer.close(); + // })(), + // (async () => { + // // Consume readable + // for await (const _ of stream.readable) { + // // Do nothing + // } + // })(), + // ]); + // running = false; + // })(); + // // Wait for running activity to finish, should complete without error + // await Promise.all([ + // randomActivityProm, + // serverStreamProms, + // randomDataProm, + // ]); + // await client.destroy({ force: true }); + // await server.stop(); + // await socket.stop(); + // }, + // { numRuns: 1 }, + // ); + // }); + // describe('keepalive', () => { + // const tlsConfig = fixtures.tlsConfigMemRSA1; + // test('connection can time out on client', async () => { + // const connectionEventProm = promise(); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig, + // verifyPeer: false, + // maxIdleTimeout: 1000, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e.detail), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // maxIdleTimeout: 100, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // // Setting no keepalive should cause the connection to time out + // // It has cleaned up due to timeout + // const clientConnection = client.connection; + // const clientTimeoutProm = promise(); + // clientConnection.addEventListener( + // 'error', + // (event: events.QUICConnectionErrorEvent) => { + // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { + // clientTimeoutProm.resolveP(); + // } + // }, + // ); + // await clientTimeoutProm.p; + // const serverConnection = await connectionEventProm.p; + // await sleep(100); + // // Server and client has cleaned up + // expect(clientConnection[destroyed]).toBeTrue(); + // expect(serverConnection[destroyed]).toBeTrue(); + // + // await client.destroy(); + // await server.stop(); + // }); + // test('connection can time out on server', async () => { + // const connectionEventProm = promise(); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig, + // verifyPeer: false, + // maxIdleTimeout: 100, + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e.detail), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // maxIdleTimeout: 1000, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // // Setting no keepalive should cause the connection to time out + // // It has cleaned up due to timeout + // const clientConnection = client.connection; + // const serverConnection = await connectionEventProm.p; + // const serverTimeoutProm = promise(); + // serverConnection.addEventListener( + // 'error', + // (event: events.QUICConnectionErrorEvent) => { + // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { + // serverTimeoutProm.resolveP(); + // } + // }, + // ); + // await serverTimeoutProm.p; + // await sleep(100); + // // Server and client has cleaned up + // expect(clientConnection[destroyed]).toBeTrue(); + // expect(serverConnection[destroyed]).toBeTrue(); + // + // await client.destroy(); + // await server.stop(); + // }); + // test('keep alive prevents timeout on client', async () => { + // const connectionEventProm = promise(); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig, + // verifyPeer: false, + // maxIdleTimeout: 20000, + // logKeys: './tmp/key1.log', + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e.detail), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // maxIdleTimeout: 100, + // }, + // keepaliveIntervalTime: 50, + // }); + // testsUtils.extractSocket(client, sockets); + // // Setting no keepalive should cause the connection to time out + // // It has cleaned up due to timeout + // const clientConnection = client.connection; + // const clientTimeoutProm = promise(); + // clientConnection.addEventListener( + // 'error', + // (event: events.QUICConnectionErrorEvent) => { + // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { + // clientTimeoutProm.resolveP(); + // } + // }, + // ); + // await connectionEventProm.p; + // // Connection would timeout after 100ms if keep alive didn't work + // await Promise.race([ + // sleep(300), + // clientTimeoutProm.p.then(() => { + // throw Error('Connection timed out'); + // }), + // ]); + // await client.destroy(); + // await server.stop(); + // }); + // test('keep alive prevents timeout on server', async () => { + // const connectionEventProm = promise(); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig, + // verifyPeer: false, + // maxIdleTimeout: 100, + // logKeys: './tmp/key1.log', + // }, + // keepaliveIntervalTime: 50, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e.detail), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // maxIdleTimeout: 20000, + // }, + // }); + // testsUtils.extractSocket(client, sockets); + // // Setting no keepalive should cause the connection to time out + // // It has cleaned up due to timeout + // const serverConnection = await connectionEventProm.p; + // const serverTimeoutProm = promise(); + // serverConnection.addEventListener( + // 'error', + // (event: events.QUICConnectionErrorEvent) => { + // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { + // serverTimeoutProm.resolveP(); + // } + // }, + // ); + // // Connection would time out after 100ms if keep alive didn't work + // await Promise.race([ + // sleep(300), + // serverTimeoutProm.p.then(() => { + // throw Error('Connection timed out'); + // }), + // ]); + // await client.destroy(); + // await server.stop(); + // }); + // test('client keep alive prevents timeout on server', async () => { + // const connectionEventProm = promise(); + // const server = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // logger: logger.getChild(QUICServer.name), + // config: { + // tlsConfig, + // verifyPeer: false, + // maxIdleTimeout: 100, + // logKeys: './tmp/key1.log', + // }, + // }); + // testsUtils.extractSocket(server, sockets); + // server.addEventListener( + // 'connection', + // (e: events.QUICServerConnectionEvent) => + // connectionEventProm.resolveP(e.detail), + // ); + // await server.start({ + // host: '127.0.0.1' as Host, + // }); + // const client = await QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: server.port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // maxIdleTimeout: 20000, + // }, + // keepaliveIntervalTime: 50, + // }); + // testsUtils.extractSocket(client, sockets); + // // Setting no keepalive should cause the connection to time out + // // It has cleaned up due to timeout + // const serverConnection = await connectionEventProm.p; + // const serverTimeoutProm = promise(); + // serverConnection.addEventListener( + // 'error', + // (event: events.QUICConnectionErrorEvent) => { + // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { + // serverTimeoutProm.resolveP(); + // } + // }, + // ); + // // Connection would time out after 100ms if keep alive didn't work + // await Promise.race([ + // sleep(300), + // serverTimeoutProm.p.then(() => { + // throw Error('Connection timed out'); + // }), + // ]); + // await client.destroy(); + // await server.stop(); + // }); + // test('Keep alive does not prevent connection timeout', async () => { + // const clientProm = QUICClient.createQUICClient({ + // host: '::ffff:127.0.0.1' as Host, + // port: 54444 as Port, + // localHost: '::' as Host, + // crypto: { + // ops: clientCrypto, + // }, + // logger: logger.getChild(QUICClient.name), + // config: { + // verifyPeer: false, + // maxIdleTimeout: 100, + // }, + // keepaliveIntervalTime: 50, + // }); + // await expect(clientProm).rejects.toThrow( + // errors.ErrorQUICConnectionTimeout, + // ); + // }); + // }); }); From b45cc2223d149251b94ac082fed6858380312e68 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 13:58:45 +1000 Subject: [PATCH 06/22] fix: removing fixtures [ci skip] --- src/errors.ts | 2 +- tests/QUICClient.test.ts | 24 +-- tests/fixtures/certFixtures.ts | 168 ------------------- tests/fixtures/certs/ecdsa1.crt | 16 -- tests/fixtures/certs/ecdsa1.key | 27 --- tests/fixtures/certs/ecdsa2.crt | 16 -- tests/fixtures/certs/ecdsa2.key | 27 --- tests/fixtures/certs/ecdsaCA.crt | 10 -- tests/fixtures/certs/ecdsaCA.key | 5 - tests/fixtures/certs/okp1.crt | 10 -- tests/fixtures/certs/okp1.key | 3 - tests/fixtures/certs/okp2.crt | 10 -- tests/fixtures/certs/okp2.key | 3 - tests/fixtures/certs/okpCA.crt | 9 - tests/fixtures/certs/okpCA.key | 3 - tests/fixtures/certs/rsa1.crt | 20 --- tests/fixtures/certs/rsa1.key | 27 --- tests/fixtures/certs/rsa2.crt | 20 --- tests/fixtures/certs/rsa2.key | 27 --- tests/fixtures/certs/rsaCA.crt | 18 -- tests/fixtures/certs/rsaCA.key | 27 --- tests/tlsUtils.ts | 277 ++++++------------------------- tests/utils.ts | 54 ++++++ 23 files changed, 120 insertions(+), 683 deletions(-) delete mode 100644 tests/fixtures/certFixtures.ts delete mode 100644 tests/fixtures/certs/ecdsa1.crt delete mode 100644 tests/fixtures/certs/ecdsa1.key delete mode 100644 tests/fixtures/certs/ecdsa2.crt delete mode 100644 tests/fixtures/certs/ecdsa2.key delete mode 100644 tests/fixtures/certs/ecdsaCA.crt delete mode 100644 tests/fixtures/certs/ecdsaCA.key delete mode 100644 tests/fixtures/certs/okp1.crt delete mode 100644 tests/fixtures/certs/okp1.key delete mode 100644 tests/fixtures/certs/okp2.crt delete mode 100644 tests/fixtures/certs/okp2.key delete mode 100644 tests/fixtures/certs/okpCA.crt delete mode 100644 tests/fixtures/certs/okpCA.key delete mode 100644 tests/fixtures/certs/rsa1.crt delete mode 100644 tests/fixtures/certs/rsa1.key delete mode 100644 tests/fixtures/certs/rsa2.crt delete mode 100644 tests/fixtures/certs/rsa2.key delete mode 100644 tests/fixtures/certs/rsaCA.crt delete mode 100644 tests/fixtures/certs/rsaCA.key diff --git a/src/errors.ts b/src/errors.ts index 75ea9ac0..9c1b8ecf 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,5 @@ import type { POJO } from '@matrixai/errors'; -import AbstractError from '@matrixai/errors'; +import AbstractError from '@matrixai/errors/dist/AbstractError'; class ErrorQUIC extends AbstractError { static description = 'QUIC error'; diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 7728c0e1..873c258e 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -11,8 +11,7 @@ import { promise } from '@/utils'; import QUICSocket from '@/QUICSocket'; import * as testsUtils from './utils'; import { tlsConfigWithCaArb, tlsConfigWithCaGENOKPArb } from './tlsUtils'; -import { sleep } from './utils'; -import * as fixtures from './fixtures/certFixtures'; +import { generateCertificate, sleep } from './utils'; // TODO: Planed changes... // 1. convert to a describe each and run tests for each kind of cert. Just for better grouping @@ -61,11 +60,15 @@ describe(QUICClient.name, () => { test( 'to ipv4 server succeeds', async () => { - const connectionEventProm = promise(); - const keyPair = testsUtils.generateKeyPairRSA(); - const certs = testsUtils.generateCertificate({ - + const keys = await testsUtils.generateKeyPairRSA(); + const privateKeyPem = (await testsUtils.keyPairRSAToPEM(keys)).privateKey; + const cert = await testsUtils.generateCertificate({ + certId: '0', + duration: 100000, + issuerPrivateKey: keys.privateKey, + subjectKeyPair: keys, }) + const connectionEventProm = promise(); const server = new QUICServer({ crypto: { key, @@ -73,9 +76,8 @@ describe(QUICClient.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - ...{ - key: tlsConfig.tlsConfig.key - }, + key: privateKeyPem, + cert: testsUtils.certToPEM(cert), verifyPeer: false, }, }); @@ -108,9 +110,7 @@ describe(QUICClient.name, () => { expect(conn.remotePort).toBe(client.port); await client.destroy(); await server.stop(); - }, - { numRuns: 10 }, - ); + }); // testProp( // 'to ipv6 server succeeds', // [tlsConfigWithCaArb], diff --git a/tests/fixtures/certFixtures.ts b/tests/fixtures/certFixtures.ts deleted file mode 100644 index a78386a9..00000000 --- a/tests/fixtures/certFixtures.ts +++ /dev/null @@ -1,168 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fc } from '@fast-check/jest'; - -function fixturePath(name: string) { - return { - certChainFromPemFile: path.resolve( - path.join(__dirname, `certs/${name}.crt`), - ), - privKeyFromPemFile: path.resolve(path.join(__dirname, `certs/${name}.key`)), - }; -} - -// Certificate fixtures -const tlsConfigFileRSACa = fixturePath('rsaCA'); -const tlsConfigFileRSA1 = fixturePath('rsa1'); -const tlsConfigFileRSA2 = fixturePath('rsa2'); -const tlsConfigFileOKPCa = fixturePath('okpCA'); -const tlsConfigFileOKP1 = fixturePath('okp1'); -const tlsConfigFileOKP2 = fixturePath('okp2'); -const tlsConfigFileECDSACa = fixturePath('ecdsaCA'); -const tlsConfigFileECDSA1 = fixturePath('ecdsa1'); -const tlsConfigFileECDSA2 = fixturePath('ecdsa2'); - -const tlsConfigMemRSACa = { - certChainPem: fs - .readFileSync(tlsConfigFileRSACa.certChainFromPemFile) - .toString(), - privKeyPem: fs.readFileSync(tlsConfigFileRSACa.privKeyFromPemFile).toString(), -}; - -/** - * This is a RSA key signed cert generated using step-cli - * This is example 1 - */ -const tlsConfigMemRSA1 = { - certChainPem: fs - .readFileSync(tlsConfigFileRSA1.certChainFromPemFile) - .toString(), - privKeyPem: fs.readFileSync(tlsConfigFileRSA1.privKeyFromPemFile).toString(), -}; - -/** - * This is a RSA key signed cert generated using step-cli - * This is example 2 - */ -const tlsConfigMemRSA2 = { - certChainPem: fs - .readFileSync(tlsConfigFileRSA2.certChainFromPemFile) - .toString(), - privKeyPem: fs.readFileSync(tlsConfigFileRSA2.privKeyFromPemFile).toString(), -}; - -const tlsConfigMemOKPCa = { - certChainPem: fs - .readFileSync(tlsConfigFileOKPCa.certChainFromPemFile) - .toString(), - privKeyPem: fs.readFileSync(tlsConfigFileOKPCa.privKeyFromPemFile).toString(), -}; - -/** - * This is a Ed25519 (OKP) key signed cert generated using step-cli - * This is example 1 - */ -const tlsConfigMemOKP1 = { - certChainPem: fs - .readFileSync(tlsConfigFileOKP1.certChainFromPemFile) - .toString(), - privKeyPem: fs.readFileSync(tlsConfigFileOKP1.privKeyFromPemFile).toString(), -}; - -/** - * This is a Ed25519 (OKP) key signed cert generated using step-cli - * This is example 2 - */ -const tlsConfigMemOKP2 = { - certChainPem: fs - .readFileSync(tlsConfigFileOKP2.certChainFromPemFile) - .toString(), - privKeyPem: fs.readFileSync(tlsConfigFileOKP2.privKeyFromPemFile).toString(), -}; - -const tlsConfigMemECDSACa = { - certChainPem: fs - .readFileSync(tlsConfigFileECDSACa.certChainFromPemFile) - .toString(), - privKeyPem: fs - .readFileSync(tlsConfigFileECDSACa.privKeyFromPemFile) - .toString(), -}; - -/** - * This is a ECDSA key signed cert generated using step-cli - * This is example 1 - */ -const tlsConfigMemECDSA1 = { - certChainPem: fs - .readFileSync(tlsConfigFileECDSA1.certChainFromPemFile) - .toString(), - privKeyPem: fs - .readFileSync(tlsConfigFileECDSA1.privKeyFromPemFile) - .toString(), -}; - -/** - * This is a ECDSA key signed cert generated using step-cli - * This is example 2 - */ -const tlsConfigMemECDSA2 = { - certChainPem: fs - .readFileSync(tlsConfigFileECDSA2.certChainFromPemFile) - .toString(), - privKeyPem: fs - .readFileSync(tlsConfigFileECDSA2.privKeyFromPemFile) - .toString(), -}; - -const tlsConfigRSAExampleArb = fc.oneof( - fc.constant(tlsConfigFileRSA1), - fc.constant(tlsConfigFileRSA2), - fc.constant(tlsConfigMemRSA1), - fc.constant(tlsConfigMemRSA2), -); - -const tlsConfigECDSAExampleArb = fc.oneof( - fc.constant(tlsConfigFileECDSA1), - fc.constant(tlsConfigFileECDSA2), - fc.constant(tlsConfigMemECDSA1), - fc.constant(tlsConfigMemECDSA2), -); - -const tlsConfigOKPExampleArb = fc.oneof( - fc.constant(tlsConfigFileOKP1), - fc.constant(tlsConfigFileOKP2), - fc.constant(tlsConfigMemOKP1), - fc.constant(tlsConfigMemOKP2), -); - -const tlsConfigExampleArb = fc.oneof( - tlsConfigRSAExampleArb, - tlsConfigECDSAExampleArb, - tlsConfigOKPExampleArb, -); - -export { - tlsConfigFileRSACa, - tlsConfigFileRSA1, - tlsConfigFileRSA2, - tlsConfigFileOKPCa, - tlsConfigFileOKP1, - tlsConfigFileOKP2, - tlsConfigFileECDSACa, - tlsConfigFileECDSA1, - tlsConfigFileECDSA2, - tlsConfigMemRSACa, - tlsConfigMemRSA1, - tlsConfigMemRSA2, - tlsConfigMemOKPCa, - tlsConfigMemOKP1, - tlsConfigMemOKP2, - tlsConfigMemECDSACa, - tlsConfigMemECDSA1, - tlsConfigMemECDSA2, - tlsConfigRSAExampleArb, - tlsConfigECDSAExampleArb, - tlsConfigOKPExampleArb, - tlsConfigExampleArb, -}; diff --git a/tests/fixtures/certs/ecdsa1.crt b/tests/fixtures/certs/ecdsa1.crt deleted file mode 100644 index 9f46399f..00000000 --- a/tests/fixtures/certs/ecdsa1.crt +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICcjCCAhmgAwIBAgIRAKWvSGgR//677j2Muzl638EwCgYIKoZIzj0EAwIwEjEQ -MA4GA1UEAxMHZWNkc2FDQTAeFw0yMzA0MTkwNTUxMDBaFw0yNDA0MTgwNTUwNTla -MBExDzANBgNVBAMTBmVjZHNhMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAOAc1hO5sm7vi7e0bhruAy2rXOWeaiu18cY6dxJHeTm+23eSzy9+NdtPCYpC -lTztRo7Ou1bLERV0+CZwlv+45I2i5N3S/fJkjv2aU5WyjY8wRz/xllussrnGhGN0 -OXgH6Mo5VZtj4dm4wYJc4VzoNnLcUA2FSCkP7Zt20wSfIoEBONqsp35xsh4eMZSc -gk59Ob0ZrpzyRH5mhhyRZzEROl3mqTpO+wgCMt1j2Yw+OX5PIS8t1nWM8vT8OmDQ -lRAIKPYq+leym3tL/6DmJ4a7yFY4zs+BLBJS5onPooruhm1MshRrj6oGWIGM/Obl -mhpI8lcPmdJm/k+z7JIavp1ReukCAwEAAaOBhTCBgjAOBgNVHQ8BAf8EBAMCB4Aw -HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTfZiCVU0fJ -Gt8AqxfCkvZ6zITx3zAfBgNVHSMEGDAWgBS+zhf4HsPvpiSlw6SEaB4viqp+zzAR -BgNVHREECjAIggZlY2RzYTEwCgYIKoZIzj0EAwIDRwAwRAIga9uUG4UpZncQFRLH -nPg0hPPeXMI8NIZawmxb0xCd9u0CIEiGubn9SBPRRFMTIW0/yxQ8QCQZDGhKTyMI -hmohYJju ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/ecdsa1.key b/tests/fixtures/certs/ecdsa1.key deleted file mode 100644 index ab4031d0..00000000 --- a/tests/fixtures/certs/ecdsa1.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA4BzWE7mybu+Lt7RuGu4DLatc5Z5qK7Xxxjp3Ekd5Ob7bd5LP -L341208JikKVPO1Gjs67VssRFXT4JnCW/7jkjaLk3dL98mSO/ZpTlbKNjzBHP/GW -W6yyucaEY3Q5eAfoyjlVm2Ph2bjBglzhXOg2ctxQDYVIKQ/tm3bTBJ8igQE42qyn -fnGyHh4xlJyCTn05vRmunPJEfmaGHJFnMRE6XeapOk77CAIy3WPZjD45fk8hLy3W -dYzy9Pw6YNCVEAgo9ir6V7Kbe0v/oOYnhrvIVjjOz4EsElLmic+iiu6GbUyyFGuP -qgZYgYz85uWaGkjyVw+Z0mb+T7Pskhq+nVF66QIDAQABAoIBAQCU0bE6kgIx9nHi -ADdpPInxxqw+cg5wjjZJesNc6QdP2DQmV9+q2vVqdzaPkB1Hwwwqgo8WWGw0wmKS -LAupLh7fCr9NqfEmB66IKvW8H0AvSFDX3lYI9EoOYQvCewg6c44a0y3MrOvpxu0A -4ypnpm+ZoECsacf8NG6E4MfJdt7TqDzy6iMDgYTyJJH1eEuQz1Wm6jvxCVhSzBRy -W7K24J4jc5XOiJcePZ7Ezd2u75zohHsqfW23pn4KOGqNfnLbDteadONWgPfepGzX -EI7BtEgoL1XRPAyH4bECn+JaQZgqCops9Sfc9Q/FEPylyQt299unmV5TSMImAdH9 -e+1NMM2BAoGBAPLcAvG+lTrdBGnASYO0nVKFp+9/F6P9fzReO437faCG4DQByCiJ -D+zF+cGUDvKYKDzaJiIKf1/vFxcJx0foX1Yr2ZpkkSEH3YUpx3eHr29yjvOnfoug -ecnE+EySNsTaOa49Ote9cZS++phtV7puJ0bxMPgYgB3Fw1L8AesMx1UxAoGBAOw9 -Ju/RTjZ9drkqA03j+JVZOOSlMlUu7cJa8ynWUEgaNHIXMzRpMEfypSglwSthRqdB -I03eSKps3YcGcLsqjjCCIwilOii3TU3fiB1PG9Go8Q1EDwpIaRlC1rrlhgds4Ypg -RDnQkDIS1v7ovCrcvhTKsgN1uq3u47AaSerDsvM5AoGAXciQaoJKZnzLI3tZ6D5r -Ml7ixx2xJ2bRJIvvO5kOnlr3YZ3+iYjEWY7qL9LZIt2XTEiByUt8fLN7my3vgtk9 -V61/TpvfX34GEVJc8M+487Std3IK8Ch8X2ps6ETeWY5cD+kdPAqwPdyyMQKoihnX -mRGy81uivdyM3RLsOrSolUECgYAVYRdj/rIwVjxnV3kDFI2Lno56isS6VsvnmemF -sMKFM/HpHVZh+N8Is9nkfz4zGdOWEVwLkQFMgxutO5T6K0jA9RYMkz9nLeeOE5uy -41TmLX7bL3yY9qKkSZs7QMhXZbAnoLyr6scR8QiJ+zAERQPix9FCZOYnFYZ/inGE -EH/iaQKBgAnzjAd0unJzFYkposfph+mpvPbnkoLbcT9Ljd3R3hKJJJ4gD+15UmXN -ctqYj7u/KjaOij8szN06G3C7VaLMadkp7mBEgOX+1Wl/W58vF/6e6LwvRvVhFrbE -wqPDl7HLDzfOP0Crwq69EFPzyWjVUkNTgHQGRWuEWsnsDpZygpOe ------END RSA PRIVATE KEY----- diff --git a/tests/fixtures/certs/ecdsa2.crt b/tests/fixtures/certs/ecdsa2.crt deleted file mode 100644 index bb024022..00000000 --- a/tests/fixtures/certs/ecdsa2.crt +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICczCCAhigAwIBAgIQQBxLmOp73KNC7UoruZXPxzAKBggqhkjOPQQDAjASMRAw -DgYDVQQDEwdlY2RzYUNBMB4XDTIzMDQxOTA1NTA0OVoXDTI0MDQxODA1NTA0OFow -ETEPMA0GA1UEAxMGZWNkc2EyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAyScrk3Yjs49GQwEKm1WNMcEJToLBcEtb9DjNxR2koHZJjy/dWRF1vSVqaesg -Hz5NCe0x/yhHOLnqjwODbGLhwrdAUZfzVy6wAc24e345FMYNS9QbY0UV/SNfkg3x -a0SWCTQ1jceksWBv2ON87nwMViZ0EKgFQWr7kBiSV7uJRmXXD2CO8AR/u3HLOjbj -v7HoCkeseUu5LxHUL464jIhZ9s++s6+k6NTa/G++dU3Z7y8LJmOVqCF4RyZkyCnR -L7hRUoxfRZhNojsYEL1kOae83oS0RE21thv8rJfC6T6sV86DWkcZJzHAI7O1cOaa -jbDfmWeP/31rxj67LHNv0Ghy+QIDAQABo4GFMIGCMA4GA1UdDwEB/wQEAwIHgDAd -BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFNecFEFMOtBG -Xfkg7rPTHvhXBJXCMB8GA1UdIwQYMBaAFL7OF/gew++mJKXDpIRoHi+Kqn7PMBEG -A1UdEQQKMAiCBmVjZHNhMjAKBggqhkjOPQQDAgNJADBGAiEAoUPnaAlJM/kS1Fbz -hgG+/tK0Le8Omr0bHxKIqUnur3ACIQDln6qhVmOw6Np9Ue1nYcpz2deuyJ++Sm61 -EFllK35v/g== ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/ecdsa2.key b/tests/fixtures/certs/ecdsa2.key deleted file mode 100644 index 24863532..00000000 --- a/tests/fixtures/certs/ecdsa2.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAyScrk3Yjs49GQwEKm1WNMcEJToLBcEtb9DjNxR2koHZJjy/d -WRF1vSVqaesgHz5NCe0x/yhHOLnqjwODbGLhwrdAUZfzVy6wAc24e345FMYNS9Qb -Y0UV/SNfkg3xa0SWCTQ1jceksWBv2ON87nwMViZ0EKgFQWr7kBiSV7uJRmXXD2CO -8AR/u3HLOjbjv7HoCkeseUu5LxHUL464jIhZ9s++s6+k6NTa/G++dU3Z7y8LJmOV -qCF4RyZkyCnRL7hRUoxfRZhNojsYEL1kOae83oS0RE21thv8rJfC6T6sV86DWkcZ -JzHAI7O1cOaajbDfmWeP/31rxj67LHNv0Ghy+QIDAQABAoIBAQC4D1+QT0nbw/5G -0W0jNeU84aXicUMPveUmU9P1ymoZaiNlIicomRFjQhldUgjSje9wZdqZ4AFLptM6 -ibx4MQLjaUsxXhkMbWwgOl3UomsY1yDTggKNk2nLd9K7koaV4Oxo/50BXu0UYWn2 -zDJFBr1DF2yc0SH9+ia2c8V1AeVaw7XisH1f0ArEo7cHeL+9O9zPrVAhM3rZa6kZ -aqwefmMxO8wDcivYWXBFBuQHDXDbIFZi/BKseGXeP6cB9qplOk3UN74tzS0jyfNx -zgMfzrAsDTHNjgxwkmgssLT0/BD4pn1vZY++0Eh4klPU2rep6jk8njyeKfGlmtxm -qV8GCgQBAoGBAN4StV49/4DrrCPo9azBM1TGvhgQvyOrTxkDLnadkKbsQIBShNBH -EwMJYespaAUIcFRE0blzLGwvqe56Y6022K36sm6zj7iwl/OCv8TMBXJXm8R3tB/g -0kLkiME0tOHkaZeEZLdSwzkHx7XG2jWMn+gsN9hEcOPAOIhgm3BtCEBlAoGBAOfi -RsjOGmW5cYXNFtYL5o7X2TjR/mTT67PGYiV68a53cGHmgLDLzzG0zKAlb9akAfyJ -Pk0kylkKfCqd+MTGciKZIBOnkZ6SDQzNpJpQmO8F9wQtC2dkl8JBQjoZauR0QBRz -YOIVl8ByJIAuFaFdRHQeZqmJ+rR9o0l3ZX9SiN0FAoGACBJKAUjjln36XbvD4imL -ghNPKXTCXbuGPnR9JdaIQWjo10EojqFmsX+PzYpNu5EY9BckQEdNYAlDdeWA5fTO -X1RzU8629JGGlFU2PyLjN8qzYKqxI096lO/VgKA3ytYQeG9ZcchSTCpaOeMmXzfj -P/8LcJLcP4rnAx3vyGBOInkCgYEAtleCD/+6VEmqEmw+y9yZ1bO4dezznbfjvf9/ -SDfQesAjWGjYw63Q5SZmTnyB4Igho8MMgb8veys6joyghaQDNl+xEKORZ98zceD3 -0f0U9ch8AQWm1QwOJkwI1wULwFAF388G3WHbbhKTz1Pt4HEmFiWd1y3QPkQZtfi0 -kJWHWD0CgYEAr8KLPxciiKBZSUvvekqlyCgFawhUHZAaQR0uaCn6Zxah8HA41iQ0 -UmtY+ZFEmcPbMgxngChDKfMaLkWjdOugzETNZQ5ljjM1wU5NLMOrxTsrjCCp/LNL -APxa+jYAk08ooQYO1tDhmyFvhikLD01kWCKPqlI1Fc4Gg7QRlADZU6g= ------END RSA PRIVATE KEY----- diff --git a/tests/fixtures/certs/ecdsaCA.crt b/tests/fixtures/certs/ecdsaCA.crt deleted file mode 100644 index ee151b77..00000000 --- a/tests/fixtures/certs/ecdsaCA.crt +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBaTCCAQ6gAwIBAgIRAO/hOT04O5h5lmtSdcNiIhAwCgYIKoZIzj0EAwIwEjEQ -MA4GA1UEAxMHZWNkc2FDQTAeFw0yMzA0MTkwNTQ5NDZaFw0yNDA0MTgwNTQ5NDZa -MBIxEDAOBgNVBAMTB2VjZHNhQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARD -3nUgyfF6SW40ce4R0DaNNsFm+c43DcdbO2G2lZTXxFLId07J8GzMOYCS4l5KsgTC -IMZ4Tr984kBiqDxPDP3go0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgw -BgEB/wIBATAdBgNVHQ4EFgQUvs4X+B7D76YkpcOkhGgeL4qqfs8wCgYIKoZIzj0E -AwIDSQAwRgIhAIb+TmcvC78w0k5bcwtTfas3hxRVpXPFBBwxR8Oay15NAiEApd3a -lE3pbVVv+cI6hf+Y/ogu8zTKf7PRawhFp7PN5ts= ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/ecdsaCA.key b/tests/fixtures/certs/ecdsaCA.key deleted file mode 100644 index 8794e684..00000000 --- a/tests/fixtures/certs/ecdsaCA.key +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN EC PRIVATE KEY----- -MHcCAQEEINqH9ILmDkBv14JmaalNy0y5dyiMxklMqkQaPFfZOgB6oAoGCCqGSM49 -AwEHoUQDQgAEQ951IMnxekluNHHuEdA2jTbBZvnONw3HWzthtpWU18RSyHdOyfBs -zDmAkuJeSrIEwiDGeE6/fOJAYqg8Twz94A== ------END EC PRIVATE KEY----- diff --git a/tests/fixtures/certs/okp1.crt b/tests/fixtures/certs/okp1.crt deleted file mode 100644 index cf2dd433..00000000 --- a/tests/fixtures/certs/okp1.crt +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBYjCCARSgAwIBAgIRAIlBBs6tfhxMUJE2l3e+e68wBQYDK2VwMBAxDjAMBgNV -BAMTBW9rcENBMB4XDTIzMDQxOTA1MTEwMFoXDTI0MDQxODA1MTEwMFowDzENMAsG -A1UEAxMEb2twMTAqMAUGAytlcAMhAJ0DqTl+cXZ1L5tkbIuFQfZw9dWNmib/V2Vd -U7Zylbz5o4GDMIGAMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcD -AQYIKwYBBQUHAwIwHQYDVR0OBBYEFFhYurVwVhKWRW3DTH4HEAb80rm0MB8GA1Ud -IwQYMBaAFGG4nEpIf1GT2/h8JF1Jgfsgb6SfMA8GA1UdEQQIMAaCBG9rcDEwBQYD -K2VwA0EAHaBEb+0Rv62+/FTgX+jyuuelHpbEfOlJceN6NbuwVeewxtb13X8c7xyj -XKK8LqD7D1fSgvIPNlNZjtMdrUeTCw== ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/okp1.key b/tests/fixtures/certs/okp1.key deleted file mode 100644 index eb54e090..00000000 --- a/tests/fixtures/certs/okp1.key +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIO5lNq23xT7UaJ+s2z1zydIsUlJ99ewck4oe0kJ2d+yb ------END PRIVATE KEY----- diff --git a/tests/fixtures/certs/okp2.crt b/tests/fixtures/certs/okp2.crt deleted file mode 100644 index bb3707aa..00000000 --- a/tests/fixtures/certs/okp2.crt +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBYjCCARSgAwIBAgIRAKqxoywMFEPX1xgyi10Rx2kwBQYDK2VwMBAxDjAMBgNV -BAMTBW9rcENBMB4XDTIzMDQxOTA1MjU0NFoXDTI0MDQxODA1MjU0NFowDzENMAsG -A1UEAxMEb2twMjAqMAUGAytlcAMhAMG9VUL3flSj/oHdi8tjffgN46Wc7qDBCKBP -//mvnFcHo4GDMIGAMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcD -AQYIKwYBBQUHAwIwHQYDVR0OBBYEFFV+KzO1iBafoQXQtPIR2G++6kB/MB8GA1Ud -IwQYMBaAFGG4nEpIf1GT2/h8JF1Jgfsgb6SfMA8GA1UdEQQIMAaCBG9rcDIwBQYD -K2VwA0EA+WDLYCDS5es1eOoIhijTunnEUkKS/HikBx80syegKXoI+t22oNk2f2SM -53ydsVzm0cHlpE/yiTFP56ZqPNonCw== ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/okp2.key b/tests/fixtures/certs/okp2.key deleted file mode 100644 index 796bec16..00000000 --- a/tests/fixtures/certs/okp2.key +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEICqEVt4e2zvDMrs4ijnyvkUb/WzE9Po9ggl440YN9nFJ ------END PRIVATE KEY----- diff --git a/tests/fixtures/certs/okpCA.crt b/tests/fixtures/certs/okpCA.crt deleted file mode 100644 index 7debf023..00000000 --- a/tests/fixtures/certs/okpCA.crt +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBIjCB1aADAgECAhBqrbwaJM78Rtc+rFDz7y6YMAUGAytlcDAQMQ4wDAYDVQQD -EwVva3BDQTAeFw0yMzA0MTkwNTA3NTRaFw0yNDA0MTgwNTA3NTRaMBAxDjAMBgNV -BAMTBW9rcENBMCowBQYDK2VwAyEA6dwiIcUEEbagcszbU8lhD2hwAm60I4YuCK33 -LfKK052jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0G -A1UdDgQWBBRhuJxKSH9Rk9v4fCRdSYH7IG+knzAFBgMrZXADQQADbu5lz38fmovr -zRfvnVsyYLM4Hr9W+LqcVdyGiTbS1jaGqCflYlWgHX0rQ1r4lgxQaRM4v+O8ztzW -XcLpRT8G ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/okpCA.key b/tests/fixtures/certs/okpCA.key deleted file mode 100644 index a828fa0b..00000000 --- a/tests/fixtures/certs/okpCA.key +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIDlS4lRpRszaon4gx/5uiMvVP2rj9l9AdVfw8v2Raw9/ ------END PRIVATE KEY----- diff --git a/tests/fixtures/certs/rsa1.crt b/tests/fixtures/certs/rsa1.crt deleted file mode 100644 index fe2c92da..00000000 --- a/tests/fixtures/certs/rsa1.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDLjCCAhagAwIBAgIRAOP1Hv5+GXJMxyBvMi6IFSAwDQYJKoZIhvcNAQELBQAw -EDEOMAwGA1UEAxMFcnNhQ0EwHhcNMjMwNDE5MDU0ODU4WhcNMjQwNDE4MDU0ODU3 -WjAPMQ0wCwYDVQQDEwRyc2ExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAu9DbhhyDmXQZhcUxmJMbUPd8w1YPnZjNTIDREWMtjRaLmeKylPL1cuL8Wmsj -wvFMbx2FhDjSXsQDzjPmFNIbT3F+i2VmolAvKUHG4JqUyfcN3E+rty1Lytvu3vDV -3OC8i1o0Y89QTXiRJIjfSBktPy3y8hskKJ6nAAbL0MQKRyKpzIV2k6epLd1Xx6dl -RtA4QHihcD5HedbD8OaVRLBbeJ/nx38Kb/vW1G6Uivd9yVdLOi306epKlks60bSW -1U5Ffy/2MkVW35/8JSyoAExOHBNix7LYaNB2ajCjLCjcrQI3AFcGuep8OPIS0MvD -y/srxR/PR1nWl3muhClGChkP+wIDAQABo4GDMIGAMA4GA1UdDwEB/wQEAwIFoDAd -BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFFFzBNyAicB+ -yUT8xh+isRcSixzkMB8GA1UdIwQYMBaAFPl6JPRYyNrrxk30v1MV8LTYwqLLMA8G -A1UdEQQIMAaCBHJzYTEwDQYJKoZIhvcNAQELBQADggEBAEQydSNhxW1v+f5J6v0K -vhWSqJmwTwhxhAvQctnophPPJb2cuyfcc0X1x+JlZFJD4eEHu5qvaYMVv7EY4Is8 -xliZbQj4L9x0246q7j44XuRlK67rj4moIoT/hLBMwRM300LIABVUHvZjAy0Fl+hs -OuVl7m1iASHoJ07mTHoKkh4tdSjIYiMoF3Nbag4hu01iJYJIK48NzvrPxCzOb4zh -eMhQG5C/2bijWMSPI112ksCV1PT9skuj4/R0bCuMzdJ/RWxOl1sF9pXACwpi/y6M -dEPebTEWIOJtHB05IMb4nLy5DoOwW6o8ZrCjVZqdNps3LCJG2PrMkYVeKYYC3ecU -Pug= ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/rsa1.key b/tests/fixtures/certs/rsa1.key deleted file mode 100644 index 0082ea7a..00000000 --- a/tests/fixtures/certs/rsa1.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAu9DbhhyDmXQZhcUxmJMbUPd8w1YPnZjNTIDREWMtjRaLmeKy -lPL1cuL8WmsjwvFMbx2FhDjSXsQDzjPmFNIbT3F+i2VmolAvKUHG4JqUyfcN3E+r -ty1Lytvu3vDV3OC8i1o0Y89QTXiRJIjfSBktPy3y8hskKJ6nAAbL0MQKRyKpzIV2 -k6epLd1Xx6dlRtA4QHihcD5HedbD8OaVRLBbeJ/nx38Kb/vW1G6Uivd9yVdLOi30 -6epKlks60bSW1U5Ffy/2MkVW35/8JSyoAExOHBNix7LYaNB2ajCjLCjcrQI3AFcG -uep8OPIS0MvDy/srxR/PR1nWl3muhClGChkP+wIDAQABAoIBADVYyrHaMYsJziIP -89lpl221DkwRitfxygZ75GZy7EJR8A8itCTYqd0bGiC4o+zASzyYfw8icKaYOJh+ -Z1DsqPM0woPnpnJeIjcuxsWPQsnnEZnNS3H5PLj/JWdPsv9NGmDem1jqTt3ibB+b -fQhmllKGw6X/DZM4FSlNxOCkfmLbuTS+yHZL5keacDcp/gx6jU4PpO+BtZcfU/3r -d5FuM+7rKsPxcUIkkPjeLwa3fnbmF/jYFsiYK+hxqsGN1t6LADM3krDYtkEF/hhq -POlo22o210xJlsABw5+LDzDBUimDfhX+ZkSvtU9xwl2g6i4MvtQkWj3aDw+19Q9S -wHFHpqkCgYEAwmSW+biAuT6q3iKmqiPeMEeAPeN9lH16vFUv+FnSU/Aezw7Jo2CO -tmvWExTj4cHnszm2gVwI5YpVhNV/yk2xGvefy0JTmli2UGzPUfSpFcnbLRaTosD5 -EQ2A0v2anf+VAjxH/lscP1lE99YWbJxZOlOhDv21aoxbNurNWAUum1UCgYEA91ao -poLGRx4Koxl/BBKO4TI8Wl/63soRegY/GSvdMwQ4E3Bo3jPb0VObhNh+uVt/ESIl -MKA6CROHib2nAaLU5OmCsRJqd4j8aeHPQY8ulrdg2sl+HTIdPEeRSWJFbWH44Wf6 -5Uly03I03wcKVS2/4jiV3RGK1IJrnvtf69GCHg8CgYAVeNTwWn/ldlWbWcp9Cp81 -SACTVXh+mWmgOeyliw5fdSuCBYkiQb0hy0GHNAyD2E5Gjr5pBYh6ClCt7oNCDljc -uLNu2TGw39GriolP7S1sGbLbBR5joNsiFhK/u4GDqEKGT7BIGTpCiRLEKWKqto5o -keo9ZKrQTRaaN09dRJ1ETQKBgFgkdseYFQblCs9i16Pg5TAVkfJ2/9UDV8UPks7q -ldKHb2BmD8kX2/8191cZO9fcZmT5sR0qDGSNpCCPCIQJviqxmJR3xp9AnwswoIZx -ex3UzdxKL/pBkK+ZpYnsTmeToXjacEvjp9r4eST9wk3mltoMZkO466udqrKmTGGi -cOc7AoGAJbcSnQAsCNolDpgJRpQZ92qC4Mnr/u0XOHBqdjBLsqOij9PiBFJBzKll -Z6nJQEeY6Z1G2/krAm5bDY18HEg0lpu8LoX3vuMg41VIXeAlecs3yux+hEHHsNph -p1KpK1I7+u+8XxX1WEbWLoWZcM7ZceygbwieNJbdvzTgf6HwUmc= ------END RSA PRIVATE KEY----- diff --git a/tests/fixtures/certs/rsa2.crt b/tests/fixtures/certs/rsa2.crt deleted file mode 100644 index 323c25bf..00000000 --- a/tests/fixtures/certs/rsa2.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDLjCCAhagAwIBAgIRAPmTX8hEiP0E2KFYhKU42SowDQYJKoZIhvcNAQELBQAw -EDEOMAwGA1UEAxMFcnNhQ0EwHhcNMjMwNDE5MDU0OTA5WhcNMjQwNDE4MDU0OTA5 -WjAPMQ0wCwYDVQQDEwRyc2EyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAvV5kAaeOv4CpFso0tp82NWTg8RDNrmSwb7kIh04rkvlKo/V5HjFQL0zfJgBF -JjH0VHRfZwIWSEcDeqwgfOMxIDOKI1xPutc43yNEp5qTSu/1L+d17NjZWUEwNn4k -r2PYtU2IgtyYH8xmLoviakAXedEPE2GgKaYm8nGEGKu+i86yfQcOj4AUV74CqcZV -reLS+nhsOwzbbFiUcKZ03oXrU2WIMhVjnsjj0LTCIzdtxdrzYNoxp5PVxYRSYgfD -zgmuel2vHgfwKOY7o3bB/iigGg4HPhQRZtD3UV/yuR/vdsJ1M9bs62qh35esLk+w -GhD1bWroN0/rhwTga1nr9DNOFwIDAQABo4GDMIGAMA4GA1UdDwEB/wQEAwIFoDAd -BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFDTwmcujYFWN -UX8I7w4Kqsm1HcfdMB8GA1UdIwQYMBaAFPl6JPRYyNrrxk30v1MV8LTYwqLLMA8G -A1UdEQQIMAaCBHJzYTIwDQYJKoZIhvcNAQELBQADggEBAACMSxCDjzTb9ddFJjih -fA7NQ8KpIxvyQuTk/W6OjRm1wJ/vwJUldECggpURRRLBX9nkXhYY3jUryVNQ5PHq -TFMqDLRdJgcPdDSgkQE8exI8gf3q6JyMpNphjJB3okFl+VSvpLINo9BaJ8gswbWc -O3vk7VNnQdtxZDApFNJgs/hfxqo970esYx1zijz5QU7O27ufVh2/93Zm9D7Wcnqn -lft+7g+0l756fOma2Y5ZodHgv0EwOb6n5Xr+SBF/cP8E2qYCwUh1FqNEtVpD+DRT -9Hw5zc6INy7DH9U7fCYcj0UOnlP7FJ/o1n3FAcp+JQULE/ZO+oEkwbBwNGN4WKmm -qdc= ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/rsa2.key b/tests/fixtures/certs/rsa2.key deleted file mode 100644 index 753b8b89..00000000 --- a/tests/fixtures/certs/rsa2.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAvV5kAaeOv4CpFso0tp82NWTg8RDNrmSwb7kIh04rkvlKo/V5 -HjFQL0zfJgBFJjH0VHRfZwIWSEcDeqwgfOMxIDOKI1xPutc43yNEp5qTSu/1L+d1 -7NjZWUEwNn4kr2PYtU2IgtyYH8xmLoviakAXedEPE2GgKaYm8nGEGKu+i86yfQcO -j4AUV74CqcZVreLS+nhsOwzbbFiUcKZ03oXrU2WIMhVjnsjj0LTCIzdtxdrzYNox -p5PVxYRSYgfDzgmuel2vHgfwKOY7o3bB/iigGg4HPhQRZtD3UV/yuR/vdsJ1M9bs -62qh35esLk+wGhD1bWroN0/rhwTga1nr9DNOFwIDAQABAoIBAH1rH5cM73kb8GE8 -qO7uwYtZdbWTKHZBD63fAawDdM6RtwKiWIZLqMy6/+v+L84dNbIdpPXnJvTVu5Mq -nxW3rtih6fKDd/bKBkua4ySSNs1h4aTJiovEDyeTCih9ITTcTzG82RW6njIpQN/B -G8K9Eg9HC9INTXNoLNFTp2ZjkP6sUdXmphrMFG3osuKj+I5sZas+1aY8zhmIzYaZ -IZXd0jt7CMMSnaDNXfLTLP0lFT6DIewnkTnkD/NueXBDSjOrFEaWVNh47qERNDvA -pr/MVTzjANrf5ijZPqPea2hGZ8Wf+rKbNZ6ky61upDGvU0t9O9slzRM4sueia3Ez -HigSVFECgYEA6ajzeL6ffcRN4f8C6XRRs5mHUFl+fq+VJHCU5RCG2Uh8wj0voCJQ -LjJHzb5Hw9WOw3RhSBsFwO23rOuNp4sUNehR46S6sx2RJ1aeKf/t9dzDuagLJWVA -iK9Q4nHIw5cUsllTHzz1x5RrFps0rgx+5YU8YrF1IoYMZ8NaWVsV348CgYEAz3le -h8CrLld8FX0LHGxmx93/rWyP2jnM37Rw/UI0+h/sC5HBwKicmZEvGqV7mJPYX2sK -B2DL54/D75PtT+RxWH/dc8FFCKimJox0vRmqd1I7fq/nl6rGDacPWFXo1p/w8tRw -xn4k3Ow7sOvj1r1ZfjO5e0iuu5P/lXJCSDlXZPkCgYEAiULRvBWnFbeS1Pb8W+4T -7MB85+uazosQvvZP0Xxy6wOVHnnZF2Xw2iJ5YdisydMTaahdWYFeUOaUpsU9UrX1 -6nEOYf8sGfp9gbF1elC5FlxYsiCQY6hmT6VFi7Cx8ow3AUpN9STXcSWz/vSv7qqy -hNxPC98ZY22BrhbpZWKHp+sCgYBg5b5QiJtqOVGqd4wI6lfWYMhYpqtIsFYO5Kcg -oRs89ku324sx/42j6zqkp0TiLaqQZd15NKwGqg5Ihj3/YiHNw2oZ9dnYAWwX9OFQ -r/v9XlqLAHyZSRjWp39zMixckLZnsvA4xBFTXMFED+eJE0YIuv+VpIx27tgfZA2w -zZcTgQKBgBODdbCPIreEkLMeq7/A4X3h12TGUYJ+HmyXZqURyxOzwB03O2Hs2Lhj -V6Efg+nciZRVwjfcRFILlsZk7QV3WtRl+4jPTMeL76btkmchgSY7XoCSa7A+GgPC -1LariTs6MnCqa7mtCFANvFUyJB6843a+dZ7W3te6G8oMF4cl/Loa ------END RSA PRIVATE KEY----- diff --git a/tests/fixtures/certs/rsaCA.crt b/tests/fixtures/certs/rsaCA.crt deleted file mode 100644 index 6cf149f8..00000000 --- a/tests/fixtures/certs/rsaCA.crt +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC8DCCAdigAwIBAgIRAJCwiiQ3TTulJhS9EK55cXUwDQYJKoZIhvcNAQELBQAw -EDEOMAwGA1UEAxMFcnNhQ0EwHhcNMjMwNDE5MDU0ODEyWhcNMjQwNDE4MDU0ODEy -WjAQMQ4wDAYDVQQDEwVyc2FDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAOPvjy4GlvFbX7iCAyt/jd0+eHlEFx/zMFwBPLZmqIA3HPJwj2cp5TY5NEWk -MM42qQLzCOmO/K4f9m6WUZw1J9tKGIEc7b+RQ/JisHz8iiFpEDayROrV4Cs/Nd0r -wvTautTXl6bd37rI4V8o0yHB0E/WcazAUJQDwvl7bVL942qB86LQqsEfxMIM7Ddq -S+9dyr67RG1MMw5Qm0889l3yDKkKOKy3I6w4dAdfCZSeZFWXl7GeIVPj1Rc6Fxt8 -Tg79Cm5lgMnmqzGBZUS4eitJq6BhTyQfoylHayCkpqhcojVDbcN5D1bH/YTUh/38 -klDaOwqCajpdu6VUr1eRGrl55A0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIG -A1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFPl6JPRYyNrrxk30v1MV8LTYwqLL -MA0GCSqGSIb3DQEBCwUAA4IBAQDIACzucf/ePefGgLVTWMVvZdfW9cXuf0Ib3zWC -6RYbA+cBBIgn0SGToY613vIeKV1uPhFpzYJd+3eFVj9x1gVHTK7Hk6o5Tf780ZKN -d4TkoTp9WER/boaOB+77nH9WopvE+ien1GM2HTgOBUrClPGCZ2OAJBq4xxDDeU2T -rxlRD06692CdVh5uSUtCIsqoCUTi7s+C9p5WG1gOVIkafFmyR2i28E2n0rKlNzXQ -UTo3936xFx8cdIJVzvPFOvMhKrOTzCCcMD8NiXXHlX51HMy3fua16W8dfLKhJp4N -1PFD57chAacE/HKlnB4LugDLCl+MBhpuyxt+Ifk0geqeV15p ------END CERTIFICATE----- diff --git a/tests/fixtures/certs/rsaCA.key b/tests/fixtures/certs/rsaCA.key deleted file mode 100644 index 8ff11adc..00000000 --- a/tests/fixtures/certs/rsaCA.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA4++PLgaW8VtfuIIDK3+N3T54eUQXH/MwXAE8tmaogDcc8nCP -ZynlNjk0RaQwzjapAvMI6Y78rh/2bpZRnDUn20oYgRztv5FD8mKwfPyKIWkQNrJE -6tXgKz813SvC9Nq61NeXpt3fusjhXyjTIcHQT9ZxrMBQlAPC+XttUv3jaoHzotCq -wR/EwgzsN2pL713KvrtEbUwzDlCbTzz2XfIMqQo4rLcjrDh0B18JlJ5kVZeXsZ4h -U+PVFzoXG3xODv0KbmWAyearMYFlRLh6K0mroGFPJB+jKUdrIKSmqFyiNUNtw3kP -Vsf9hNSH/fySUNo7CoJqOl27pVSvV5EauXnkDQIDAQABAoIBADGE9BhNujFJZqxR -IpsqpQlx7v55eoSoctMqOaRu/SCN2K4bIiyJf5CeMOsIVsoWOfW4EzGarO6vZvxz -j9td+QC9QkGapVQ/HQHlyu38d432o10esbZLLN54Zx1byokjk/91ry7DeT29MqlX -1za4Qqd1sVCUn2zovE0zMXccTXHvzJYIJ+NhbiqJZj4xtrJIkH7/5g3ad2dd1YDs -kTyycNAVXjkw2IalMtEeMfQ12RuU3opXTsIW6USlcLaYajypEtEoon1Zw486XN9M -9zdRzOX52v+6kR4QbXITeXz1Uz9t0Panf6/+5thE6FBduwmcG1wfrFJ4hkBAN2iL -sbPUO8ECgYEA+jhX+rgaFbfUolrvkgBcPaPtlwq5gUn13O6ekZl5m8nFKL6IjM/0 -p9WlO0DDW5faam3tjW9q2s0YBGEL0FrZuZxOwHzrEnx062U6oKsbT+uzCXfwju4y -dw/B9xctq+FoOuXKxdMwz3oBVa86pixEoNig8wecNbxVr7AlxNy/+SUCgYEA6TNw -axJ0vJj0DuTb398f/tGCdzvMda5AsWHXEFiq+UJoldX2LEinTxVVyBljVdBd4V3o -S82k0xncpU0Qf6BM5R2v1vSyOLY8n/X4NmqfabLC5U6WacS+zUGF4W+eOBXFHLS/ -+TAwPzXNQKs1y+88JvBFWtK6LJt1K3CPb1SeTskCgYEA5KzViubxv+NvSql8xJvb -sOG94woEGupy7eSNulbe0seFjgUDWl07JJ+WEB7R60VOcXHhZh7rMue5CVd/qn08 -+eql9jizxQNE/1RWTjvSDCpGR2L70ERUjocyJxVhwfGQhjM4K68krpi724j3OqK+ -XZldDn5n/pwOWy/KdK2iLJkCgYBhdHm6hU/7sP/taX7po9k/KFcZdQgQ9e1bM8Qo -tKVe3X3PgEcMYqBo0EW1BccZiaZyFEiLxTjob5piCj6m11rLNQjTEBea062qO6Sr -OQu9pFMGeT0qnmoOZR+KApdgYNirEm5XuOewws8wA1zMCQJeU3LobcAX/C+PI88N -e5Nc4QKBgQD4sG2yQ8/p894+r4me2GCqTR19p2yztR842NXeke8WK5vApvxa4B92 -TLiC0bxS3YzyFjG+ZFgQynrHrsehMkf3q5928Z/v7fDOeVDc9HDIult+LvlPuZ0U -cMeF6H/GFX/0gUOLowPjqk+FEgLM62sOoUPFkaH9EN6HMKr4cSB7hg== ------END RSA PRIVATE KEY----- diff --git a/tests/tlsUtils.ts b/tests/tlsUtils.ts index 53d57280..34d93ae6 100644 --- a/tests/tlsUtils.ts +++ b/tests/tlsUtils.ts @@ -6,7 +6,7 @@ import * as asn1Pkcs8 from '@peculiar/asn1-pkcs8'; import { fc } from '@fast-check/jest'; import { Crypto } from '@peculiar/webcrypto'; import sodium from 'sodium-native'; -import * as certFixtures from './fixtures/certFixtures'; +import * as testsUtils from './utils'; /** * WebCrypto polyfill from @peculiar/webcrypto @@ -106,182 +106,12 @@ function certToPEM(cert: X509Certificate): string { return cert.toString('pem') + '\n'; } -const extendedKeyUsageFlags = { - serverAuth: '1.3.6.1.5.5.7.3.1', - clientAuth: '1.3.6.1.5.5.7.3.2', - codeSigning: '1.3.6.1.5.5.7.3.3', - emailProtection: '1.3.6.1.5.5.7.3.4', - timeStamping: '1.3.6.1.5.5.7.3.8', - ocspSigning: '1.3.6.1.5.5.7.3.9', -}; - -/** - * Generate x509 certificate. - * Duration is in seconds. - * X509 certificates currently use `UTCTime` format for `notBefore` and `notAfter`. - * This means: - * - Only second resolution. - * - Minimum date for validity is 1970-01-01T00:00:00Z (inclusive). - * - Maximum date for valdity is 2049-12-31T23:59:59Z (inclusive). - */ -async function generateCertificate({ - certId, - subjectKeyPair, - issuerPrivateKey, - duration, - subjectAttrsExtra = [], - issuerAttrsExtra = [], - now = new Date(), -}: { - certId: string; - subjectKeyPair: { - publicKey: Buffer; - privateKey: Buffer; - }; - issuerPrivateKey: Buffer; - duration: number; - subjectAttrsExtra?: Array<{ [key: string]: Array }>; - issuerAttrsExtra?: Array<{ [key: string]: Array }>; - now?: Date; -}): Promise { - const certIdNum = parseInt(certId); - const iss = certIdNum === 0 ? certIdNum : certIdNum - 1; - const sub = certIdNum; - const subjectPublicCryptoKey = await importPublicKey( - subjectKeyPair.publicKey, - ); - const subjectPrivateCryptoKey = await importPrivateKey( - subjectKeyPair.privateKey, - ); - const issuerPrivateCryptoKey = await importPrivateKey(issuerPrivateKey); - if (duration < 0) { - throw new RangeError('`duration` must be positive'); - } - // X509 `UTCTime` format only has resolution of seconds - // this truncates to second resolution - const notBeforeDate = new Date(now.getTime() - (now.getTime() % 1000)); - const notAfterDate = new Date(now.getTime() - (now.getTime() % 1000)); - // If the duration is 0, then only the `now` is valid - notAfterDate.setSeconds(notAfterDate.getSeconds() + duration); - if (notBeforeDate < new Date(0)) { - throw new RangeError( - '`notBeforeDate` cannot be before 1970-01-01T00:00:00Z', - ); - } - if (notAfterDate > new Date(new Date('2050').getTime() - 1)) { - throw new RangeError('`notAfterDate` cannot be after 2049-12-31T23:59:59Z'); - } - const serialNumber = certId; - // The entire subject attributes and issuer attributes - // is constructed via `x509.Name` class - // By default this supports on a limited set of names: - // CN, L, ST, O, OU, C, DC, E, G, I, SN, T - // If custom names are desired, this needs to change to constructing - // `new x509.Name('FOO=BAR', { FOO: '1.2.3.4' })` manually - // And each custom attribute requires a registered OID - // Because the OID is what is encoded into ASN.1 - const subjectAttrs = [ - { - CN: [`${sub}`], - }, - // Filter out conflicting CN attributes - ...subjectAttrsExtra.filter((attr) => !('CN' in attr)), - ]; - const issuerAttrs = [ - { - CN: [`${iss}`], - }, - // Filter out conflicting CN attributes - ...issuerAttrsExtra.filter((attr) => !('CN' in attr)), - ]; - const certConfig = { - serialNumber, - notBefore: notBeforeDate, - notAfter: notAfterDate, - subject: subjectAttrs, - issuer: issuerAttrs, - signingAlgorithm: { - name: 'EdDSA', - }, - publicKey: subjectPublicCryptoKey, - signingKey: subjectPrivateCryptoKey, - extensions: [ - new x509.BasicConstraintsExtension(true, undefined, true), - new x509.KeyUsagesExtension( - x509.KeyUsageFlags.keyCertSign | - x509.KeyUsageFlags.cRLSign | - x509.KeyUsageFlags.digitalSignature | - x509.KeyUsageFlags.nonRepudiation | - x509.KeyUsageFlags.keyAgreement | - x509.KeyUsageFlags.keyEncipherment | - x509.KeyUsageFlags.dataEncipherment, - true, - ), - new x509.ExtendedKeyUsageExtension([ - extendedKeyUsageFlags.serverAuth, - extendedKeyUsageFlags.clientAuth, - extendedKeyUsageFlags.codeSigning, - extendedKeyUsageFlags.emailProtection, - extendedKeyUsageFlags.timeStamping, - extendedKeyUsageFlags.ocspSigning, - ]), - await x509.SubjectKeyIdentifierExtension.create(subjectPublicCryptoKey), - ] as Array, - }; - certConfig.signingKey = issuerPrivateCryptoKey; - return await x509.X509CertificateGenerator.create(certConfig); -} - type KeyPair = { privateKey: Buffer; publicKey: Buffer; }; -async function createTLSConfigWithChain( - keyPairs: Array, - generateCertId?: () => string, -): Promise<{ - certChainPem: string; - privKeyPem: string; - caPem: string; -}> { - if (keyPairs.length === 0) throw Error('Must have at least 1 keypair'); - let num = -1; - const defaultNumGen = () => { - num += 1; - return `${num}`; - }; - generateCertId = generateCertId ?? defaultNumGen; - let previousCert: X509Certificate | null = null; - let previousKeyPair: KeyPair | null = null; - const certChain: Array = []; - for (const keyPair of keyPairs) { - const certId = generateCertId(); - const newCert = await generateCertificate({ - certId, - duration: 31536000, - issuerPrivateKey: previousKeyPair?.privateKey ?? keyPair.privateKey, - subjectKeyPair: keyPair, - issuerAttrsExtra: previousCert?.subjectName.toJSON(), - }); - certChain.unshift(newCert); - previousCert = newCert; - previousKeyPair = keyPair; - } - let certChainPEM = ''; - let caPem: string | null = null; - for (const certificate of certChain) { - const pem = certToPEM(certificate); - caPem = pem; - certChainPEM += pem; - } - return { - privKeyPem: privateKeyToPEM(previousKeyPair!.privateKey), - certChainPem: certChainPEM, - caPem: caPem!, - }; -} /** * Extracts Ed25519 Public Key from Ed25519 Private Key @@ -325,63 +155,62 @@ const keyPairsArb = (min: number = 1, max?: number) => size: 'xsmall', }); -const tlsConfigArb = (keyPairs: fc.Arbitrary> = keyPairsArb()) => - keyPairs - .map(async (keyPairs) => await createTLSConfigWithChain(keyPairs)) - .noShrink(); - -const tlsConfigWithCaRSAArb = fc.record({ - type: fc.constant('RSA'), - ca: fc.constant(certFixtures.tlsConfigMemRSACa), - tlsConfig: certFixtures.tlsConfigRSAExampleArb, -}); - -const tlsConfigWithCaOKPArb = fc.record({ - type: fc.constant('OKP'), - ca: fc.constant(certFixtures.tlsConfigMemOKPCa), - tlsConfig: certFixtures.tlsConfigOKPExampleArb, -}); - -const tlsConfigWithCaECDSAArb = fc.record({ - type: fc.constant('ECDSA'), - ca: fc.constant(certFixtures.tlsConfigMemECDSACa), - tlsConfig: certFixtures.tlsConfigECDSAExampleArb, -}); - -const tlsConfigWithCaGENOKPArb = tlsConfigArb().map(async (configProm) => { - const config = await configProm; - return { - type: fc.constant('GEN-OKP'), - tlsConfig: { - certChainPem: config.certChainPem, - privKeyPem: config.privKeyPem, - }, - ca: { - certChainPem: config.caPem, - privKeyPem: '', - }, - }; -}); - -const tlsConfigWithCaArb = fc - .oneof( - tlsConfigWithCaRSAArb, - tlsConfigWithCaOKPArb, - tlsConfigWithCaECDSAArb, - tlsConfigWithCaGENOKPArb, - ) - .noShrink(); +// const tlsConfigArb = (keyPairs: fc.Arbitrary> = keyPairsArb()) => +// keyPairs +// .map(async (keyPairs) => await createTLSConfigWithChain(keyPairs)) +// .noShrink(); + +// const tlsConfigWithCaRSAArb = fc.record({ +// type: fc.constant('RSA'), +// ca: fc.constant(certFixtures.tlsConfigMemRSACa), +// tlsConfig: certFixtures.tlsConfigRSAExampleArb, +// }); +// +// const tlsConfigWithCaOKPArb = fc.record({ +// type: fc.constant('OKP'), +// ca: fc.constant(certFixtures.tlsConfigMemOKPCa), +// tlsConfig: certFixtures.tlsConfigOKPExampleArb, +// }); +// +// const tlsConfigWithCaECDSAArb = fc.record({ +// type: fc.constant('ECDSA'), +// ca: fc.constant(certFixtures.tlsConfigMemECDSACa), +// tlsConfig: certFixtures.tlsConfigECDSAExampleArb, +// }); + +// const tlsConfigWithCaGENOKPArb = tlsConfigArb().map(async (configProm) => { +// const config = await configProm; +// return { +// type: fc.constant('GEN-OKP'), +// tlsConfig: { +// certChainPem: config.certChainPem, +// privKeyPem: config.privKeyPem, +// }, +// ca: { +// certChainPem: config.caPem, +// privKeyPem: '', +// }, +// }; +// }); +// +// const tlsConfigWithCaArb = fc +// .oneof( +// tlsConfigWithCaRSAArb, +// tlsConfigWithCaOKPArb, +// tlsConfigWithCaECDSAArb, +// tlsConfigWithCaGENOKPArb, +// ) +// .noShrink(); export { - generateCertificate, privateKeyArb, publicKeyArb, keyPairArb, keyPairsArb, - tlsConfigArb, - tlsConfigWithCaArb, - tlsConfigWithCaRSAArb, - tlsConfigWithCaOKPArb, - tlsConfigWithCaECDSAArb, - tlsConfigWithCaGENOKPArb, + // tlsConfigArb, + // tlsConfigWithCaArb, + // tlsConfigWithCaRSAArb, + // tlsConfigWithCaOKPArb, + // tlsConfigWithCaECDSAArb, + // tlsConfigWithCaGENOKPArb, }; diff --git a/tests/utils.ts b/tests/utils.ts index ff4ce934..359c6e23 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -469,6 +469,59 @@ async function generateCertificate({ return await x509.X509CertificateGenerator.create(certConfig); } +// FIXME: +// async function createTLSConfigWithChain( +// keyPairs: Array<{ +// publicKey: JsonWebKey; +// privateKey: JsonWebKey; +// }>, +// generateCertId?: () => string, +// ): Promise<{ +// certChainPem: string; +// privKeyPem: string; +// caPem: string; +// }> { +// if (keyPairs.length === 0) throw Error('Must have at least 1 keypair'); +// let num = -1; +// const defaultNumGen = () => { +// num += 1; +// return `${num}`; +// }; +// generateCertId = generateCertId ?? defaultNumGen; +// let previousCert: X509Certificate | null = null; +// let previousKeyPair: { +// publicKey: JsonWebKey; +// privateKey: JsonWebKey; +// } | null = null; +// const certChain: Array = []; +// for (const keyPair of keyPairs) { +// const certId = generateCertId(); +// const newCert = await generateCertificate({ +// certId, +// duration: 31536000, +// issuerPrivateKey: previousKeyPair?.privateKey ?? keyPair.privateKey, +// subjectKeyPair: keyPair, +// issuerAttrsExtra: previousCert?.subjectName.toJSON(), +// }); +// certChain.unshift(newCert); +// previousCert = newCert; +// previousKeyPair = keyPair; +// } +// let certChainPEM = ''; +// let caPem: string | null = null; +// for (const certificate of certChain) { +// const pem = certToPEM(certificate); +// caPem = pem; +// certChainPEM += pem; +// } +// +// return { +// privKeyPem: privateKeyToPEM(previousKeyPair!.privateKey), +// certChainPem: certChainPEM, +// caPem: caPem!, +// }; +// } + function certToPEM(cert: X509Certificate): string { return cert.toString('pem') + '\n'; } @@ -641,6 +694,7 @@ export { keyPairECDSAToPEM, keyPairEd25519ToPEM, generateCertificate, + // createTLSConfigWithChain, FIXME certToPEM, generateKeyHMAC, signHMAC, From a2058ccd6fc7ff8ca0c07751d86fc46b3083046f Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 15:38:15 +1000 Subject: [PATCH 07/22] tests: fixing up tests [ci skip] --- src/QUICClient.ts | 12 ++++++++++-- src/QUICConnection.ts | 8 +++++++- src/QUICServer.ts | 5 ++++- src/QUICSocket.ts | 8 +++++--- tests/QUICClient.test.ts | 22 +++++++++++++++++----- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 111dc65e..94e98b21 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -207,12 +207,17 @@ class QUICClient extends EventTarget { ctx.signal.addEventListener('abort', (r) => { abortController.abort(r); }); + console.log('bsd'); try { await Promise.race([ - await connection.start({ ...ctx, signal: abortController.signal }), - socketErrorP, + connection.start({ ...ctx, signal: abortController.signal }), + socketErrorP.catch(e => { + console.error(e); + throw e; + }), ]); } catch (e) { + console.error(e); // In case the `connection.start` is on-going, we need to abort it abortController.abort(e); if (!isSocketShared) { @@ -223,13 +228,16 @@ class QUICClient extends EventTarget { } finally { socket.removeEventListener('socketError', handleQUICSocketError); } + console.log('bsd') const client = new this({ socket, connection, isSocketShared, logger, }); + console.log('bsd') address = utils.buildAddress(host_, port); + console.log('bsd') logger.info(`Created ${this.name} to ${address}`); return client; } diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 9a2ee2e1..b73e6869 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -353,7 +353,11 @@ class QUICConnection extends EventTarget { this.socket.connectionMap.set(this.connectionId, this); // Waits for the first short packet after establishment // This ensures that TLS has been established and verified on both sides - await this.secureEstablishedP; + console.log('sending'); + await this.send(); + console.log('waiting secured'); + // await this.secureEstablishedP; + this.logger.warn('secured'); // After this is done // We need to established the keep alive interval time if (this.config.keepAliveIntervalTime != null) { @@ -496,6 +500,7 @@ class QUICConnection extends EventTarget { }, }; try { + this.logger.debug(`recv ${data.byteLength} bytes`); // This can process concatenated QUIC packets // This may mutate `data` this.conn.recv(data, recvInfo); @@ -684,6 +689,7 @@ class QUICConnection extends EventTarget { sendInfo.to.port, sendInfo.to.host, ); + this.logger.debug(`sent ${sendLength} bytes`); } } catch (e) { // If called `stop` due to an error here diff --git a/src/QUICServer.ts b/src/QUICServer.ts index 281a4c5c..e81fd75d 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -54,7 +54,9 @@ class QUICServer extends EventTarget { protected connectionMap: QUICConnectionMap; protected handleQUICSocketEvents = (e: events.QUICSocketEvent) => { - this.dispatchEvent(e); + const event = new Event('asd'); + this.dispatchEvent(event); + this.dispatchEvent(event); if (e instanceof events.QUICSocketErrorEvent) { this.dispatchEvent( new events.QUICServerErrorEvent({ @@ -339,6 +341,7 @@ class QUICServer extends EventTarget { ), }); await connection.start(); // TODO: pass ctx + console.log('dispatching'); this.dispatchEvent( new events.QUICServerConnectionEvent({ detail: connection }), ); diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index dc879647..a601392a 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -13,6 +13,7 @@ import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; import * as errors from './errors'; +import { status } from '@matrixai/async-init/dist/utils'; /** * Events: @@ -100,7 +101,8 @@ class QUICSocket extends EventTarget { } // If the connection has already stopped running // then we discard the packet. - if (!connection[running]) { + if (!(connection[running] || connection[status] === 'starting')) { + console.log('x'); return; } // Acquire the conn lock, this ensures mutual exclusion @@ -254,8 +256,8 @@ class QUICSocket extends EventTarget { `Cannot stop QUICSocket with ${this.connectionMap.size} active connection(s)`, ); } - this.socket.off('message', this.handleSocketMessage); - this.socket.off('error', this.handleSocketError); + this.socket.removeListener('message', this.handleSocketMessage); + this.socket.removeListener('error', this.handleSocketError); await this.socketClose(); this.dispatchEvent(new events.QUICSocketStopEvent()); this.logger.info(`Stopped ${this.constructor.name} on ${address}`); diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 873c258e..b35882f2 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -10,7 +10,6 @@ import * as errors from '@/errors'; import { promise } from '@/utils'; import QUICSocket from '@/QUICSocket'; import * as testsUtils from './utils'; -import { tlsConfigWithCaArb, tlsConfigWithCaGENOKPArb } from './tlsUtils'; import { generateCertificate, sleep } from './utils'; // TODO: Planed changes... @@ -18,7 +17,7 @@ import { generateCertificate, sleep } from './utils'; // 2. Almost none of the tests need to be fast check, convert to standard tests. describe(QUICClient.name, () => { - const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ + const logger = new Logger(`${QUICClient.name} Test`, LogLevel.INFO, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -68,7 +67,9 @@ describe(QUICClient.name, () => { issuerPrivateKey: keys.privateKey, subjectKeyPair: keys, }) + console.log('asd'); const connectionEventProm = promise(); + console.log('asd'); const server = new QUICServer({ crypto: { key, @@ -81,15 +82,19 @@ describe(QUICClient.name, () => { verifyPeer: false, }, }); + console.log('asd'); testsUtils.extractSocket(server, sockets); + console.log('asd'); server.addEventListener( - 'connection', + 'serverConnection', (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); + console.log('asd'); await server.start({ host: '127.0.0.1' as Host, }); + console.log('asd'); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, port: server.port, @@ -100,16 +105,23 @@ describe(QUICClient.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, + logKeys: './tmp/key.log' }, }); + console.log('asd'); testsUtils.extractSocket(client, sockets); + console.log('asd'); const conn = (await connectionEventProm.p).detail; + console.log('asd'); expect(conn.localHost).toBe('127.0.0.1'); expect(conn.localPort).toBe(server.port); expect(conn.remoteHost).toBe('127.0.0.1'); expect(conn.remotePort).toBe(client.port); - await client.destroy(); - await server.stop(); + console.log('asd'); + // await client.destroy(); + // console.log('asd'); + // await server.stop(); + // console.log('asd'); }); // testProp( // 'to ipv6 server succeeds', From 1d55f125887708bcdbe46b02010d75d751456d06 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 15:42:29 +1000 Subject: [PATCH 08/22] dep: bump `@matrixai/async-init` to `1.8.4` [ci skip] --- package-lock.json | 29 ++++++----------------------- package.json | 2 +- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21463b70..e77fd840 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@matrixai/async-cancellable": "^1.1.0", - "@matrixai/async-init": "^1.8.3", + "@matrixai/async-init": "^1.8.4", "@matrixai/async-locks": "^4.0.0", "@matrixai/contexts": "^1.0.0", "@matrixai/errors": "^1.1.7", @@ -1184,24 +1184,14 @@ "integrity": "sha512-f0yxu7dHwvffZ++7aCm2WIcCJn18uLcOTdCCwEA3R3KVHYE3TG/JNoTWD9/mqBkAV1AI5vBfJzg27WnF9rOUXQ==" }, "node_modules/@matrixai/async-init": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.8.3.tgz", - "integrity": "sha512-AQyffo14v1PpfwevAdJ0sCSl3kMh5KK312s5S3/fgM7AosT5ovtYflWhi1Th9hxGL0mhSu4VwmDwcyeyjsyCgw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.8.4.tgz", + "integrity": "sha512-33cGC7kHTs9KKwMHJA5d5XURWhx3QUq7lLxPEXLoVfWdTHixcWNvtfshAOso0hbRfx1P3ZSgsb+ZHaIASHhWfg==", "dependencies": { - "@matrixai/async-locks": "^3.2.0", + "@matrixai/async-locks": "^4.0.0", "@matrixai/errors": "^1.1.7" } }, - "node_modules/@matrixai/async-init/node_modules/@matrixai/async-locks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-3.2.0.tgz", - "integrity": "sha512-Gl919y3GK2lBCI7M3MabE2u0+XOhKqqgwFEGVaPSI2BrdSI+RY7K3+dzjTSUTujVZwiYskT611CBvlDm9fhsNg==", - "dependencies": { - "@matrixai/errors": "^1.1.3", - "@matrixai/resources": "^1.1.4", - "async-mutex": "^0.3.2" - } - }, "node_modules/@matrixai/async-locks": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-4.0.0.tgz", @@ -2224,14 +2214,6 @@ "node": ">=8" } }, - "node_modules/async-mutex": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", - "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", - "dependencies": { - "tslib": "^2.3.1" - } - }, "node_modules/babel-jest": { "version": "28.1.3", "dev": true, @@ -6044,6 +6026,7 @@ }, "node_modules/tslib": { "version": "2.5.0", + "dev": true, "license": "0BSD" }, "node_modules/tsutils": { diff --git a/package.json b/package.json index b2f60004..7c467de5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@matrixai/async-cancellable": "^1.1.0", "@matrixai/contexts": "^1.0.0", - "@matrixai/async-init": "^1.8.3", + "@matrixai/async-init": "^1.8.4", "@matrixai/async-locks": "^4.0.0", "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^3.1.0", From 9f6ae63dcb44453b93a4e88f02a1345e1939dcbd Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 17:00:59 +1000 Subject: [PATCH 09/22] feat: added in secure establishment event [ci skip] --- src/QUICConnection.ts | 152 +++++++++++++++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 31 deletions(-) diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index b73e6869..06c03890 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -23,6 +23,7 @@ import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; import * as errors from './errors'; +import { never } from './utils'; /** * Think of this as equivalent to `net.Socket`. @@ -193,6 +194,12 @@ class QUICConnection extends EventTarget { public readonly lockbox = new LockBox(); public readonly lockCode = 'Lock'; // TODO: more unique code + protected customVerified = false; + protected shortReceived = false; + protected shortSent = false; + protected secured = false; + protected count = 0; + public constructor({ type, scid, @@ -353,10 +360,8 @@ class QUICConnection extends EventTarget { this.socket.connectionMap.set(this.connectionId, this); // Waits for the first short packet after establishment // This ensures that TLS has been established and verified on both sides - console.log('sending'); await this.send(); - console.log('waiting secured'); - // await this.secureEstablishedP; + await this.secureEstablishedP; this.logger.warn('secured'); // After this is done // We need to established the keep alive interval time @@ -447,6 +452,49 @@ class QUICConnection extends EventTarget { // But during `start` we are just waiting this.socket.connectionMap.delete(this.connectionId); + // Emit error if peer error + const peerError = this.conn.peerError(); + if (peerError != null) { + const message = `Connection errored out with peerError ${Buffer.from( + peerError.reason, + ).toString()}(${peerError.errorCode})`; + this.logger.info(message); + this.dispatchEvent( + new events.QUICConnectionErrorEvent({ + detail: new errors.ErrorQUICConnectionInternal( + message, + { + data: { + type: 'local', + ...peerError, + } + }, + ), + }), + ); + } + + const localError = this.conn.localError(); + if (localError != null) { + const message = `connection failed with localError ${Buffer.from( + localError.reason, + ).toString()}(${localError.errorCode})`; + this.logger.info(message); + this.dispatchEvent( + new events.QUICConnectionErrorEvent({ + detail: new errors.ErrorQUICConnectionInternal( + message, + { + data: { + type: 'local', + ...localError, + } + } + ), + }), + ); + } + this.dispatchEvent(new events.QUICConnectionStopEvent()); this.logger.info(`Stopped ${this.constructor.name}`); } @@ -518,39 +566,37 @@ class QUICConnection extends EventTarget { this.lastErrorMessage = e.message; } + // Checking if the packet was a short frame. + // Short indicates that the peer has completed TLS verification + if (!this.shortReceived) { + const header = quiche.Header.fromSlice(data, quiche.MAX_CONN_ID_LEN); + // if short frame + if (header.ty === 5) { + this.shortReceived = true; + this.conn.sendAckEliciting(); + } + } + + if ( + !this.secured && + this.shortReceived && + this.shortReceived && + !this.conn.isDraining() + ) { + if (this.count >= 1) { + this.secured = true; + this.resolveSecureEstablishedP(); + // this.dispatchEvent(new events.QUICConnectionRemoteSecureEvent()); TODO + } + this.count += 1; + } + // We don't actually "fail" // the closedP until we proceed // But note that if there's an error if (this.conn.isEstablished()) { this.resolveEstablishedP(); - - if (this.type === 'server') { - // For server connections, if we are established - // we are secure established - this.resolveSecureEstablishedP(); - } else if (this.type === 'client') { - // We need a hueristic to indicate whether we are securely established - // If we are already established - // AND IF, we are getting a packet after establishment - // And we didn't result in an error - // Neither draining, nor closed, nor timed out - // For server connections - // If we are already established, then we are secure established - // To know if the server is also established - // We need to know the NEXT recv after we are already established - // So we received something, and that allows us to be established - // UPON the next recv - // We need to ensure: - // 1. No errors - // 2. Not draining - // 3. No - // YES the main thing is that there is no errors - // I think that's the KEY - // But we must only switch - // If were "already" established - // That this wasn't the first time we were established - } } // We also need to know whether this is our first short frame @@ -586,7 +632,7 @@ class QUICConnection extends EventTarget { } readIds.push(quicStream.streamId); quicStream.read(); - // QuicStream.dispatchEvent(new events.QUICStreamReadableEvent()); // TODO: remove? + // QuicStream.dispatchEvent(new events.QUICStreamReadablaeEvent()); // TODO: remove? } if (readIds.length > 0) { this.logger.info(`processed reads for ${readIds}`); @@ -691,6 +737,50 @@ class QUICConnection extends EventTarget { ); this.logger.debug(`sent ${sendLength} bytes`); } + // Handling custom TLS verification, this must be done after the following conditions. + // 1. Connection established. + // 2. Certs available. + // 3. Sent after connection has established. + if ( + !this.customVerified && + this.conn.isEstablished() && + this.conn.peerCertChain() != null + ) { + this.customVerified = true; + const peerCerts = this.conn.peerCertChain(); + if (peerCerts == null) never(); + const peerCertsPem = peerCerts.map((c) => + utils.certificateDERToPEM(c), + ); + // Dispatching certs available event + // this.dispatchEvent(new events.QUICConnectionRemoteCertEvent()); TODO + try { + // if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); TODO + this.conn.sendAckEliciting(); + } catch (e) { + // Force the connection to end. + // Error 304 indicates cert chain failed verification. + // Error 372 indicates cert chain was missing. + this.conn.close( + false, + 304, + Buffer.from(`Custom TLSFail: ${e.message}`), + ); + } + } + + // Check the header type + if (!this.shortSent) { + const header = quiche.Header.fromSlice( + sendBuffer, + quiche.MAX_CONN_ID_LEN, + ); + // If short frame + if (header.ty === 5) { + // Short was sent, locally secured + this.shortSent = true; + } + } } catch (e) { // If called `stop` due to an error here // we MUST not call `this.send` again From 225babf5cda3e8ed897bd6c5277df5affa9ee39a Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 17:22:06 +1000 Subject: [PATCH 10/22] feat: added `verifyCallback` and `verifyAllowFail` [ci skip] --- src/QUICClient.ts | 6 +++++- src/QUICConnection.ts | 10 +++++++++- src/QUICServer.ts | 7 ++++++- src/config.ts | 3 +++ src/native/napi/config.rs | 11 ++++++++++- src/native/types.ts | 1 + src/types.ts | 11 +++++++++++ 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 94e98b21..f036def1 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -1,6 +1,6 @@ import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ContextTimed } from '@matrixai/contexts'; -import type { ClientCrypto, Host, Hostname, Port } from './types'; +import type { ClientCrypto, Host, Hostname, Port, VerifyCallback } from './types'; import type { Config } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; import type { @@ -79,6 +79,7 @@ class QUICClient extends EventTarget { resolveHostname?: (hostname: Hostname) => Host | PromiseLike; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; logger?: Logger; }, ctx?: Partial, @@ -96,6 +97,7 @@ class QUICClient extends EventTarget { resolveHostname = utils.resolveHostname, reasonToCode, codeToReason, + verifyCallback, logger = new Logger(`${this.name}`), }: { host: Host | Hostname; @@ -112,6 +114,7 @@ class QUICClient extends EventTarget { resolveHostname?: (hostname: Hostname) => Host | PromiseLike; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; logger?: Logger; }, @context ctx: ContextTimed, @@ -199,6 +202,7 @@ class QUICClient extends EventTarget { config: quicConfig, reasonToCode, codeToReason, + verifyCallback, logger: logger.getChild( `${QUICConnection.name} ${scid.toString().slice(32)}`, ), diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 06c03890..5f3f49c9 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -25,6 +25,9 @@ import * as utils from './utils'; import * as errors from './errors'; import { never } from './utils'; +// FIXME +type VerifyCallback = (certs: Array) => void; + /** * Think of this as equivalent to `net.Socket`. * Errors here are emitted to the connection only. @@ -199,6 +202,7 @@ class QUICConnection extends EventTarget { protected shortSent = false; protected secured = false; protected count = 0; + protected verifyCallback: VerifyCallback | undefined; public constructor({ type, @@ -209,6 +213,7 @@ class QUICConnection extends EventTarget { socket, reasonToCode = () => 0, codeToReason = (type, code) => new Error(`${type} ${code}`), + verifyCallback, logger, }: | { @@ -220,6 +225,7 @@ class QUICConnection extends EventTarget { socket: QUICSocket; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; logger?: Logger; } | { @@ -231,6 +237,7 @@ class QUICConnection extends EventTarget { socket: QUICSocket; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; logger?: Logger; }) { super(); @@ -281,6 +288,7 @@ class QUICConnection extends EventTarget { this.config = config; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; + this.verifyCallback = verifyCallback; this._remoteHost = remoteInfo.host; this._remotePort = remoteInfo.port; const { @@ -755,7 +763,7 @@ class QUICConnection extends EventTarget { // Dispatching certs available event // this.dispatchEvent(new events.QUICConnectionRemoteCertEvent()); TODO try { - // if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); TODO + if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); this.conn.sendAckEliciting(); } catch (e) { // Force the connection to end. diff --git a/src/QUICServer.ts b/src/QUICServer.ts index e81fd75d..902319e8 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -6,7 +6,7 @@ import type { StreamCodeToReason, StreamReasonToCode, QUICConfig, - ServerCrypto, + ServerCrypto, VerifyCallback } from './types'; import type { Header } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; @@ -51,6 +51,7 @@ class QUICServer extends EventTarget { protected socket: QUICSocket; protected reasonToCode: StreamReasonToCode | undefined; protected codeToReason: StreamCodeToReason | undefined; + protected verifyCallback: VerifyCallback | undefined; protected connectionMap: QUICConnectionMap; protected handleQUICSocketEvents = (e: events.QUICSocketEvent) => { @@ -77,6 +78,7 @@ class QUICServer extends EventTarget { resolveHostname = utils.resolveHostname, reasonToCode, codeToReason, + verifyCallback, logger, }: { crypto: { @@ -98,6 +100,7 @@ class QUICServer extends EventTarget { resolveHostname?: (hostname: Hostname) => Host | PromiseLike; reasonToCode?: StreamReasonToCode; codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; logger?: Logger; }) { super(); @@ -124,6 +127,7 @@ class QUICServer extends EventTarget { this.config = quicConfig; this.reasonToCode = reasonToCode; this.codeToReason = codeToReason; + this.verifyCallback = verifyCallback; } @ready(new errors.ErrorQUICServerNotRunning()) @@ -336,6 +340,7 @@ class QUICServer extends EventTarget { config: this.config, reasonToCode: this.reasonToCode, codeToReason: this.codeToReason, + verifyCallback: this.verifyCallback, logger: this.logger.getChild( `${QUICConnection.name} ${scid.toString().slice(32)}-${clientConnRef}`, ), diff --git a/src/config.ts b/src/config.ts index 5c9a1249..ae61a30e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,6 +26,7 @@ const sigalgs = [ const clientDefault: QUICConfig = { sigalgs, verifyPeer: true, + verifyAllowFail: false, grease: true, maxIdleTimeout: 0, maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 @@ -45,6 +46,7 @@ const clientDefault: QUICConfig = { const serverDefault: QUICConfig = { sigalgs, verifyPeer: false, + verifyAllowFail: false, grease: true, maxIdleTimeout: 0, maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 @@ -148,6 +150,7 @@ function buildQuicheConfig(config: QUICConfig): QuicheConfig { try { quicheConfig = quiche.Config.withBoringSslCtx( config.verifyPeer, + config.verifyAllowFail, caPEMBuffer, keyPEMBuffers, certChainPEMBuffers, diff --git a/src/native/napi/config.rs b/src/native/napi/config.rs index b8272cd8..81dcc234 100644 --- a/src/native/napi/config.rs +++ b/src/native/napi/config.rs @@ -50,6 +50,7 @@ impl Config { #[napi(factory)] pub fn with_boring_ssl_ctx( verify_peer: bool, + verify_allow_fail: bool, ca: Option, key: Option>, cert: Option>, @@ -65,7 +66,15 @@ impl Config { } else { boring::ssl::SslVerifyMode::NONE }; - ssl_ctx_builder.set_verify(verify_value); + ssl_ctx_builder.set_verify_callback(verify_value, move |pre_verify, _| { + // Override any validation errors, this is needed so we can request certs but validate them + // manually. + if verify_allow_fail { + true + } else { + pre_verify + } + }); // Setup all CA certificates if let Some(ca) = ca { let mut x509_store_builder = boring::x509::store::X509StoreBuilder::new() diff --git a/src/native/types.ts b/src/native/types.ts index b7f42693..81536b5f 100644 --- a/src/native/types.ts +++ b/src/native/types.ts @@ -45,6 +45,7 @@ interface ConfigConstructor { new (): Config; withBoringSslCtx( verifyPeer: boolean, + verifyAllowFail: boolean, ca?: Uint8Array | undefined | null, key?: Array | undefined | null, cert?: Array | undefined | null, diff --git a/src/types.ts b/src/types.ts index 34971a38..d6887b10 100644 --- a/src/types.ts +++ b/src/types.ts @@ -168,9 +168,17 @@ type QUICConfig = { * Verify the other peer. * Clients by default set this to true. * Servers by default set this to false. + * Servers will not request peer certs unless this is true. + * Server certs are always sent */ verifyPeer: boolean; + /** + * Will allow unsecure TLS certs, allowing for certs to be requested + * but the verification result is ignored. + */ + verifyAllowFail: boolean; + /** * Enables the logging of secret keys to a file path. * Use this with wireshark to decrypt the QUIC packets for debugging. @@ -299,6 +307,8 @@ type QUICConfig = { enableEarlyData: boolean; }; +type VerifyCallback = (certs: Array) => void; + export type { Opaque, Callback, @@ -320,4 +330,5 @@ export type { QUICConfig, ContextCancellable, ContextTimed, + VerifyCallback, }; From 9cc653dec75443161badb62234ebdbe632871552 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 23 Jun 2023 17:24:34 +1000 Subject: [PATCH 11/22] tests: fixing up client tests - Fixed up start erroring out. - Fixed up graceful TLS test. - Cleaning up. [ci skip] --- src/QUICClient.ts | 36 +- src/QUICConnection.ts | 136 +- src/QUICServer.ts | 11 +- src/QUICSocket.ts | 28 +- src/errors.ts | 2 +- tests/QUICClient.test.ts | 2719 ++++++++++++++++++-------------------- tests/tlsUtils.ts | 6 +- tests/utils.ts | 65 +- 8 files changed, 1453 insertions(+), 1550 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index f036def1..addfb5ab 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -1,6 +1,12 @@ import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ContextTimed } from '@matrixai/contexts'; -import type { ClientCrypto, Host, Hostname, Port, VerifyCallback } from './types'; +import type { + ClientCrypto, + Host, + Hostname, + Port, + VerifyCallback, +} from './types'; import type { Config } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; import type { @@ -48,6 +54,8 @@ class QUICClient extends EventTarget { protected config: Config; protected _connection: QUICConnection; protected connectionMap: QUICConnectionMap; + // Used to track address string for logging ONLY + protected address: string; /** * Creates a QUIC Client @@ -211,17 +219,12 @@ class QUICClient extends EventTarget { ctx.signal.addEventListener('abort', (r) => { abortController.abort(r); }); - console.log('bsd'); try { await Promise.race([ connection.start({ ...ctx, signal: abortController.signal }), - socketErrorP.catch(e => { - console.error(e); - throw e; - }), + socketErrorP, ]); } catch (e) { - console.error(e); // In case the `connection.start` is on-going, we need to abort it abortController.abort(e); if (!isSocketShared) { @@ -232,16 +235,14 @@ class QUICClient extends EventTarget { } finally { socket.removeEventListener('socketError', handleQUICSocketError); } - console.log('bsd') + address = utils.buildAddress(host_, port); const client = new this({ socket, connection, isSocketShared, + address, logger, }); - console.log('bsd') - address = utils.buildAddress(host_, port); - console.log('bsd') logger.info(`Created ${this.name} to ${address}`); return client; } @@ -339,11 +340,13 @@ class QUICClient extends EventTarget { socket, isSocketShared, connection, + address, logger, }: { socket: QUICSocket; isSocketShared: boolean; connection: QUICConnection; + address: string; logger: Logger; }) { super(); @@ -351,6 +354,7 @@ class QUICClient extends EventTarget { this.socket = socket; this.isSocketShared = isSocketShared; this._connection = connection; + this.address = address; // Listen on all socket events socket.addEventListener('socketError', this.handleQUICSocketEvents); socket.addEventListener('socketStop', this.handleQUICSocketEvents); @@ -402,25 +406,25 @@ class QUICClient extends EventTarget { }: { force?: boolean; } = {}) { - const address = utils.buildAddress(this.socket.host, this.socket.port); + const address = this.address; this.logger.info(`Destroy ${this.constructor.name} on ${address}`); // Listen on all socket events this.socket.removeEventListener('socketError', this.handleQUICSocketEvents); this.socket.removeEventListener('socketStop', this.handleQUICSocketEvents); // Listen on all connection events - this.connection.removeEventListener( + this._connection.removeEventListener( 'connectionStream', this.handleQUICConnectionEvents, ); - this.connection.removeEventListener( + this._connection.removeEventListener( 'connectionStop', this.handleQUICConnectionEvents, ); - this.connection.removeEventListener( + this._connection.removeEventListener( 'connectionError', this.handleQUICConnectionEvents, ); - this.connection.removeEventListener( + this._connection.removeEventListener( 'streamDestroy', this.handleQUICConnectionEvents, ); diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 5f3f49c9..09525666 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -2,19 +2,24 @@ import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ContextTimed } from '@matrixai/contexts'; import type QUICSocket from './QUICSocket'; import type QUICConnectionId from './QUICConnectionId'; -import type { Host, Port, RemoteInfo, StreamId } from './types'; +import type { + Host, + Port, + QUICConfig, + RemoteInfo, + StreamCodeToReason, + StreamId, + StreamReasonToCode, +} from './types'; import type { Connection, ConnectionErrorCode, SendInfo } from './native/types'; -import type { StreamCodeToReason, StreamReasonToCode } from './types'; -import type { QUICConfig } from './types'; -import { Monitor, RWLockWriter } from '@matrixai/async-locks'; +import { Lock, LockBox, Monitor, RWLockWriter } from '@matrixai/async-locks'; import { - StartStop, ready, - status, running, + StartStop, + status, } from '@matrixai/async-init/dist/StartStop'; import Logger from '@matrixai/logger'; -import { Lock, LockBox } from '@matrixai/async-locks'; import { Timer } from '@matrixai/timer'; import { context, timedCancellable } from '@matrixai/contexts/dist/decorators'; import { buildQuicheConfig } from './config'; @@ -22,8 +27,8 @@ import QUICStream from './QUICStream'; import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; -import * as errors from './errors'; import { never } from './utils'; +import * as errors from './errors'; // FIXME type VerifyCallback = (certs: Array) => void; @@ -369,7 +374,45 @@ class QUICConnection extends EventTarget { // Waits for the first short packet after establishment // This ensures that TLS has been established and verified on both sides await this.send(); - await this.secureEstablishedP; + await this.secureEstablishedP.catch((e) => { + this.socket.connectionMap.delete(this.connectionId); + + if (this.conn.isTimedOut()) { + // We don't dispatch an event here, it was already done in the timeout. + throw new errors.ErrorQUICConnectionStartTimeOut(); + } + + // Emit error if local error + const localError = this.conn.localError(); + if (localError != null) { + const message = `connection start failed with localError ${Buffer.from( + localError.reason, + ).toString()}(${localError.errorCode})`; + this.logger.info(message); + throw new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...localError, + }, + }); + } + // Emit error if peer error + const peerError = this.conn.peerError(); + if (peerError != null) { + const message = `Connection start failed with peerError ${Buffer.from( + peerError.reason, + ).toString()}(${peerError.errorCode})`; + this.logger.info(message); + throw new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...peerError, + }, + }); + } + // Throw the default error if none of the above were true, this shouldn't really happen + throw e; + }); this.logger.warn('secured'); // After this is done // We need to established the keep alive interval time @@ -460,6 +503,14 @@ class QUICConnection extends EventTarget { // But during `start` we are just waiting this.socket.connectionMap.delete(this.connectionId); + if (this.conn.isTimedOut()) { + this.dispatchEvent( + new events.QUICConnectionErrorEvent({ + detail: new errors.ErrorQUICConnectionIdleTimeOut(), + }), + ); + } + // Emit error if peer error const peerError = this.conn.peerError(); if (peerError != null) { @@ -469,15 +520,12 @@ class QUICConnection extends EventTarget { this.logger.info(message); this.dispatchEvent( new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionInternal( - message, - { - data: { - type: 'local', - ...peerError, - } + detail: new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...peerError, }, - ), + }), }), ); } @@ -490,15 +538,12 @@ class QUICConnection extends EventTarget { this.logger.info(message); this.dispatchEvent( new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionInternal( - message, - { - data: { - type: 'local', - ...localError, - } - } - ), + detail: new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...localError, + }, + }), }), ); } @@ -578,7 +623,7 @@ class QUICConnection extends EventTarget { // Short indicates that the peer has completed TLS verification if (!this.shortReceived) { const header = quiche.Header.fromSlice(data, quiche.MAX_CONN_ID_LEN); - // if short frame + // If short frame if (header.ty === 5) { this.shortReceived = true; this.conn.sendAckEliciting(); @@ -594,7 +639,7 @@ class QUICConnection extends EventTarget { if (this.count >= 1) { this.secured = true; this.resolveSecureEstablishedP(); - // this.dispatchEvent(new events.QUICConnectionRemoteSecureEvent()); TODO + // This.dispatchEvent(new events.QUICConnectionRemoteSecureEvent()); TODO } this.count += 1; } @@ -757,9 +802,7 @@ class QUICConnection extends EventTarget { this.customVerified = true; const peerCerts = this.conn.peerCertChain(); if (peerCerts == null) never(); - const peerCertsPem = peerCerts.map((c) => - utils.certificateDERToPEM(c), - ); + const peerCertsPem = peerCerts.map((c) => utils.certificateDERToPEM(c)); // Dispatching certs available event // this.dispatchEvent(new events.QUICConnectionRemoteCertEvent()); TODO try { @@ -833,6 +876,7 @@ class QUICConnection extends EventTarget { const connTimeOutHandler = async () => { // This can only be called when the timeout has occurred // This transitions the connection state + this.logger.debug('CALLING ON TIMEOUT'); this.conn.onTimeout(); // At this point... @@ -847,26 +891,16 @@ class QUICConnection extends EventTarget { // So if it is timed out, it gets closed too // But if it is is timed out due to idle we raise an error - if (this.conn.isTimedOut()) { - // This is just a dispatch on the connection error - // Note that this may cause the client to attempt - // to stop the socket and stuff - // The client should ignore this event error - // Because it's actually being handled - // On the other hand... - // If we randomly fail here - // It's correct to properly raise an event - // To bubble up.... - - this.dispatchEvent( - new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionIdleTimeOut(), - }), - ); - } - // At the same time, we may in fact be closed too if (this.conn.isClosed()) { + // If it was still starting waiting for the secure event, + // we need to reject that promise. + if (this[status] === 'starting') { + this.rejectSecureEstablishedP( + new errors.ErrorQUICConnectionInternal('Connection has closed!'), + ); + } + // We actually finally closed here // Actually the question is that this could be an error // The act of closing is an error? @@ -903,8 +937,9 @@ class QUICConnection extends EventTarget { const timeout = this.conn.timeout(); // If this is `null`, then technically there's nothing to do if (timeout == null) return; + // Allow an extra 1ms for the delay to fully complete so we can avoid a repeated 0ms delay this.connTimeOutTimer = new Timer({ - delay: timeout, + delay: timeout + 1, handler: connTimeOutHandler, }); }; @@ -916,6 +951,7 @@ class QUICConnection extends EventTarget { if (this.connTimeOutTimer != null) { this.connTimeOutTimer.cancel(); } + this.logger.debug(`timeout created with delay ${timeout}`); this.connTimeOutTimer = new Timer({ delay: timeout, handler: connTimeOutHandler, diff --git a/src/QUICServer.ts b/src/QUICServer.ts index 902319e8..1489f96a 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -6,7 +6,8 @@ import type { StreamCodeToReason, StreamReasonToCode, QUICConfig, - ServerCrypto, VerifyCallback + ServerCrypto, + VerifyCallback, } from './types'; import type { Header } from './native/types'; import type QUICConnectionMap from './QUICConnectionMap'; @@ -345,8 +346,12 @@ class QUICServer extends EventTarget { `${QUICConnection.name} ${scid.toString().slice(32)}-${clientConnRef}`, ), }); - await connection.start(); // TODO: pass ctx - console.log('dispatching'); + try { + await connection.start(); // TODO: pass ctx + } catch (e) { + // Ignoring any errors here as a failure to connect + return; + } this.dispatchEvent( new events.QUICServerConnectionEvent({ detail: connection }), ); diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index a601392a..3ea9909c 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -7,13 +7,13 @@ import Logger from '@matrixai/logger'; import { running } from '@matrixai/async-init'; import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; import { Monitor, RWLockWriter } from '@matrixai/async-locks'; +import { status } from '@matrixai/async-init/dist/utils'; import QUICConnectionId from './QUICConnectionId'; import QUICConnectionMap from './QUICConnectionMap'; import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; import * as errors from './errors'; -import { status } from '@matrixai/async-init/dist/utils'; /** * Events: @@ -85,6 +85,8 @@ class QUICSocket extends EventTarget { return; } // At this point, the connection may not yet be started + // FIXME: How can we be awaiting connection secured event WHILE + // processing packets for it? const connection_ = await this.server.connectionNew( remoteInfo_, header, @@ -102,18 +104,22 @@ class QUICSocket extends EventTarget { // If the connection has already stopped running // then we discard the packet. if (!(connection[running] || connection[status] === 'starting')) { - console.log('x'); return; } // Acquire the conn lock, this ensures mutual exclusion // for state changes on the internal connection - const mon = new Monitor(connection.lockbox, RWLockWriter); - await mon.withF(connection.lockCode, async (mon) => { - // Even if we are `stopping`, the `quiche` library says we need to - // continue processing any packets. - await connection.recv(data, remoteInfo_, mon); - await connection.send(mon); - }); + try { + const mon = new Monitor(connection.lockbox, RWLockWriter); + await mon.withF(connection.lockCode, async (mon) => { + // Even if we are `stopping`, the `quiche` library says we need to + // continue processing any packets. + await connection.recv(data, remoteInfo_, mon); + await connection.send(mon); + }); + } catch (e) { + // Race condition with destroying socket, just ignore + if (!(e instanceof errors.ErrorQUICSocketNotRunning)) throw e; + } }; /** @@ -256,8 +262,8 @@ class QUICSocket extends EventTarget { `Cannot stop QUICSocket with ${this.connectionMap.size} active connection(s)`, ); } - this.socket.removeListener('message', this.handleSocketMessage); - this.socket.removeListener('error', this.handleSocketError); + this.socket.off('message', this.handleSocketMessage); + this.socket.off('error', this.handleSocketError); await this.socketClose(); this.dispatchEvent(new events.QUICSocketStopEvent()); this.logger.info(`Stopped ${this.constructor.name} on ${address}`); diff --git a/src/errors.ts b/src/errors.ts index 9c1b8ecf..6ede7054 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -104,7 +104,7 @@ class ErrorQUICConnectionIdleTimeOut extends ErrorQUICConnection { */ class ErrorQUICConnectionInternal extends ErrorQUICConnection { static description = 'QUIC Connection internal conn error'; - public data: { + public declare data: { type: 'local' | 'remote'; isApp: boolean; errorCode: number; diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index b35882f2..f480dc0d 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -3,25 +3,23 @@ import type * as events from '@/events'; import type QUICConnection from '@/QUICConnection'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; import { fc, testProp } from '@fast-check/jest'; -import { destroyed } from '@matrixai/async-init'; +import { running } from '@matrixai/async-init'; +import QUICSocket from '@/QUICSocket'; import QUICClient from '@/QUICClient'; import QUICServer from '@/QUICServer'; import * as errors from '@/errors'; import { promise } from '@/utils'; -import QUICSocket from '@/QUICSocket'; import * as testsUtils from './utils'; -import { generateCertificate, sleep } from './utils'; +import { KeyTypes, sleep, TLSConfigs } from './utils'; -// TODO: Planed changes... -// 1. convert to a describe each and run tests for each kind of cert. Just for better grouping -// 2. Almost none of the tests need to be fast check, convert to standard tests. describe(QUICClient.name, () => { - const logger = new Logger(`${QUICClient.name} Test`, LogLevel.INFO, [ + const logger = new Logger(`${QUICClient.name} Test`, LogLevel.DEBUG, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), ]); + const localhost = '127.0.0.1' as Host; // This has to be setup asynchronously due to key generation const serverCrypto: ServerCrypto = { sign: testsUtils.signHMAC, @@ -33,14 +31,8 @@ describe(QUICClient.name, () => { }; let sockets: Set; - let tlsConfigServer : { - key: string, - cert: string, - }; - let tlsConfigClient : { - key: string, - cert: string, - }; + const defaultType = 'RSA'; + const types: Array = ['RSA', 'ECDSA', 'ED25519']; // We need to test the stream making beforeEach(async () => { @@ -56,20 +48,649 @@ describe(QUICClient.name, () => { }); // Are we describing a dual stack client!? describe('dual stack client', () => { - test( - 'to ipv4 server succeeds', - async () => { - const keys = await testsUtils.generateKeyPairRSA(); - const privateKeyPem = (await testsUtils.keyPairRSAToPEM(keys)).privateKey; - const cert = await testsUtils.generateCertificate({ - certId: '0', - duration: 100000, - issuerPrivateKey: keys.privateKey, - subjectKeyPair: keys, - }) - console.log('asd'); + test('to ipv4 server succeeds', async () => { + const tlsConfigServer = await testsUtils.generateConfig(defaultType); + + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigServer.key, + cert: tlsConfigServer.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + logKeys: './tmp/key.log', + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + expect(conn.localHost).toBe('127.0.0.1'); + expect(conn.localPort).toBe(server.port); + expect(conn.remoteHost).toBe('127.0.0.1'); + expect(conn.remotePort).toBe(client.port); + await client.destroy(); + await server.stop(); + }); + test('to ipv6 server succeeds', async () => { + const connectionEventProm = promise(); + const tlsConfigServer = await testsUtils.generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigServer.key, + cert: tlsConfigServer.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e), + ); + await server.start({ + host: '::1' as Host, + port: 0 as Port, + }); + const client = await QUICClient.createQUICClient({ + host: '::1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + logKeys: './tmp/key.log', + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + expect(conn.localHost).toBe('::1'); + expect(conn.localPort).toBe(server.port); + expect(conn.remoteHost).toBe('::1'); + expect(conn.remotePort).toBe(client.port); + await client.destroy(); + await server.stop(); + }); + test('to dual stack server succeeds', async () => { + const connectionEventProm = promise(); + const tlsConfigServer = await testsUtils.generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigServer.key, + cert: tlsConfigServer.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e), + ); + await server.start({ + host: '::' as Host, + port: 0 as Port, + }); + const client = await QUICClient.createQUICClient({ + host: '::' as Host, // Will resolve to ::1 + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + expect(conn.localHost).toBe('::'); + expect(conn.localPort).toBe(server.port); + expect(conn.remoteHost).toBe('::1'); + expect(conn.remotePort).toBe(client.port); + await client.destroy(); + await server.stop(); + }); + }); + describe('hard connection failures', () => { + test('times out with maxIdleTimeout when there is no server', async () => { + // QUICClient repeatedly dials until the connection timeout + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + verifyPeer: false, + }, + }), + ).rejects.toThrow(errors.ErrorQUICConnectionStartTimeOut); + }); + test.todo('client times out with ctx timer while starting'); + test.todo('client aborted while starting'); + test.todo('client times out after connection stops responding'); + test.todo('server times out after connection stops responding'); + test.todo('server handles socket error'); + test.todo('client handles socket error'); + }); + describe.each(types)('TLS rotation with %s', (type) => { + test('existing connections config is unchanged and still function', async () => { + const tlsConfig1 = await testsUtils.generateConfig(type); + const tlsConfig2 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig1.key, + cert: tlsConfig1.cert, + }, + }); + testsUtils.extractSocket(server, sockets); + await server.start({ + host: localhost, + }); + const client1 = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + verifyAllowFail: true, + }, + }); + testsUtils.extractSocket(client1, sockets); + const peerCertChainInitial = client1.connection.conn.peerCertChain(); + server.updateConfig({ + key: tlsConfig2.key, + cert: tlsConfig2.cert, + }); + // The existing connection's certs should be unchanged + const peerCertChainNew = client1.connection.conn.peerCertChain(); + expect(peerCertChainNew![0].toString()).toStrictEqual( + peerCertChainInitial![0].toString(), + ); + await client1.destroy(); + await server.stop(); + }); + test('new connections use new config', async () => { + const tlsConfig1 = await testsUtils.generateConfig(type); + const tlsConfig2 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig1.key, + cert: tlsConfig1.cert, + }, + }); + testsUtils.extractSocket(server, sockets); + await server.start({ + host: localhost, + }); + const client1 = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + verifyAllowFail: true, + }, + }); + testsUtils.extractSocket(client1, sockets); + const peerCertChainInitial = client1.connection.conn.peerCertChain(); + server.updateConfig({ + key: tlsConfig2.key, + cert: tlsConfig2.cert, + }); + // Starting a new connection has a different peerCertChain + const client2 = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + verifyAllowFail: true, + }, + }); + testsUtils.extractSocket(client2, sockets); + const peerCertChainNew = client2.connection.conn.peerCertChain(); + expect(peerCertChainNew![0].toString()).not.toStrictEqual( + peerCertChainInitial![0].toString(), + ); + await client1.destroy(); + await client2.destroy(); + await server.stop(); + }); + }); + describe.each(types)('graceful tls handshake with %s certs', (type) => { + test('server verification succeeds', async () => { + const tlsConfigs = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs.key, + cert: tlsConfigs.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + handleConnectionEventProm.resolveP, + ); + await server.start({ + host: localhost, + }); + // Connection should succeed + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + ca: tlsConfigs.ca, + }, + }); + testsUtils.extractSocket(client, sockets); + await handleConnectionEventProm.p; + await client.destroy(); + await server.stop(); + }); + test('client verification succeeds', async () => { + const tlsConfigs1 = await testsUtils.generateConfig(type); + const tlsConfigs2 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs1.key, + cert: tlsConfigs1.cert, + verifyPeer: true, + ca: tlsConfigs2.ca, + }, + }); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + handleConnectionEventProm.resolveP, + ); + await server.start({ + host: localhost, + }); + // Connection should succeed + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + key: tlsConfigs2.key, + cert: tlsConfigs2.cert, + verifyPeer: false, + }, + }); + await client.destroy(); + await server.stop(); + }); + test('client and server verification succeeds', async () => { + const tlsConfigs1 = await testsUtils.generateConfig(type); + const tlsConfigs2 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs1.key, + cert: tlsConfigs1.cert, + ca: tlsConfigs2.ca, + verifyPeer: true, + }, + }); + testsUtils.extractSocket(server, sockets); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + handleConnectionEventProm.resolveP, + ); + await server.start({ + host: localhost, + }); + // Connection should succeed + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + key: tlsConfigs2.key, + cert: tlsConfigs2.cert, + ca: tlsConfigs1.ca, + verifyPeer: true, + }, + }); + testsUtils.extractSocket(client, sockets); + await handleConnectionEventProm.p; + await client.destroy(); + await server.stop(); + }); + test('graceful failure verifying server', async () => { + const tlsConfigs1 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs1.key, + cert: tlsConfigs1.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + await server.start({ + host: localhost, + }); + // Connection should fail + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + }, + }), + ).toReject(); + await server.stop(); + }); + test('graceful failure verifying client', async () => { + const tlsConfigs1 = await testsUtils.generateConfig(type); + const tlsConfigs2 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs1.key, + cert: tlsConfigs1.cert, + verifyPeer: true, + }, + }); + testsUtils.extractSocket(server, sockets); + await server.start({ + host: localhost, + }); + // Connection should fail + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + key: tlsConfigs2.key, + cert: tlsConfigs2.cert, + verifyPeer: false, + }, + }), + ).toReject(); + await server.stop(); + }); + test('graceful failure verifying client and server', async () => { + const tlsConfigs1 = await testsUtils.generateConfig(type); + const tlsConfigs2 = await testsUtils.generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs1.key, + cert: tlsConfigs1.cert, + verifyPeer: true, + }, + }); + testsUtils.extractSocket(server, sockets); + await server.start({ + host: localhost, + }); + // Connection should fail + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + key: tlsConfigs2.key, + cert: tlsConfigs2.cert, + verifyPeer: true, + }, + }), + ).toReject(); + + await server.stop(); + }); + }); + // FIXME: These tests are failing pending the stream changes. + describe.skip('handles random packets', () => { + testProp( + 'client handles random noise from server', + [ + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + ], + async (data, messages) => { + const tlsConfig = await testsUtils.generateConfig('RSA'); + const socket = new QUICSocket({ + logger: logger.getChild('socket'), + }); + await socket.start({ + host: '127.0.0.1' as Host, + }); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + socket, + }); + testsUtils.extractSocket(server, sockets); const connectionEventProm = promise(); - console.log('asd'); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const serverStreamProms: Array> = []; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable.pipeTo(stream.writable); + serverStreamProms.push(streamProm); + }, + ); + // Sending random data to client from the perspective of the server + let running = true; + const randomDataProm = (async () => { + let count = 0; + while (running) { + await socket.send( + data[count % data.length], + client.port, + '127.0.0.1', + ); + await sleep(5); + count += 1; + } + })(); + // We want to check that things function fine between bad data + const randomActivityProm = (async () => { + const stream = await client.connection.streamNew(); + await Promise.all([ + (async () => { + // Write data + const writer = stream.writable.getWriter(); + for (const message of messages) { + await writer.write(message); + await sleep(7); + } + await writer.close(); + })(), + (async () => { + // Consume readable + for await (const _ of stream.readable) { + // Do nothing + } + })(), + ]); + running = false; + })(); + // Wait for running activity to finish, should complete without error + await Promise.all([ + randomActivityProm, + serverStreamProms, + randomDataProm, + ]); + await client.destroy({ force: true }); + await server.stop(); + await socket.stop(); + }, + { numRuns: 1 }, + ); + testProp( + 'client handles random noise from external', + [ + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + ], + async (data, messages) => { + const tlsConfig = await testsUtils.generateConfig('RSA'); + const socket = new QUICSocket({ + logger: logger.getChild('socket'), + }); + await socket.start({ + host: '127.0.0.1' as Host, + }); const server = new QUICServer({ crypto: { key, @@ -77,24 +698,21 @@ describe(QUICClient.name, () => { }, logger: logger.getChild(QUICServer.name), config: { - key: privateKeyPem, - cert: testsUtils.certToPEM(cert), + key: tlsConfig.key, + cert: tlsConfig.cert, verifyPeer: false, }, }); - console.log('asd'); testsUtils.extractSocket(server, sockets); - console.log('asd'); + const connectionEventProm = promise(); server.addEventListener( 'serverConnection', (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); - console.log('asd'); await server.start({ host: '127.0.0.1' as Host, }); - console.log('asd'); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, port: server.port, @@ -105,1439 +723,614 @@ describe(QUICClient.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - logKeys: './tmp/key.log' }, }); - console.log('asd'); testsUtils.extractSocket(client, sockets); - console.log('asd'); const conn = (await connectionEventProm.p).detail; - console.log('asd'); - expect(conn.localHost).toBe('127.0.0.1'); - expect(conn.localPort).toBe(server.port); - expect(conn.remoteHost).toBe('127.0.0.1'); - expect(conn.remotePort).toBe(client.port); - console.log('asd'); - // await client.destroy(); - // console.log('asd'); - // await server.stop(); - // console.log('asd'); + // Do the test + const serverStreamProms: Array> = []; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable.pipeTo(stream.writable); + serverStreamProms.push(streamProm); + }, + ); + // Sending random data to client from the perspective of the server + let running = true; + const randomDataProm = (async () => { + let count = 0; + while (running) { + await socket.send( + data[count % data.length], + client.port, + '127.0.0.1', + ); + await sleep(5); + count += 1; + } + })(); + // We want to check that things function fine between bad data + const randomActivityProm = (async () => { + const stream = await client.connection.streamNew(); + await Promise.all([ + (async () => { + // Write data + const writer = stream.writable.getWriter(); + for (const message of messages) { + await writer.write(message); + await sleep(7); + } + await writer.close(); + })(), + (async () => { + // Consume readable + for await (const _ of stream.readable) { + // Do nothing + } + })(), + ]); + running = false; + })(); + // Wait for running activity to finish, should complete without error + await Promise.all([ + randomActivityProm, + serverStreamProms, + randomDataProm, + ]); + await client.destroy({ force: true }); + await server.stop(); + await socket.stop(); + }, + { numRuns: 1 }, + ); + testProp( + 'server handles random noise from client', + [ + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + ], + async (data, messages) => { + const tlsConfig = await testsUtils.generateConfig('RSA'); + const socket = new QUICSocket({ + logger: logger.getChild('socket'), + }); + await socket.start({ + host: '127.0.0.1' as Host, + }); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + const connectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '127.0.0.1' as Host, + port: server.port, + socket, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const serverStreamProms: Array> = []; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable.pipeTo(stream.writable); + serverStreamProms.push(streamProm); + }, + ); + // Sending random data to client from the perspective of the server + let running = true; + const randomDataProm = (async () => { + let count = 0; + while (running) { + await socket.send( + data[count % data.length], + server.port, + '127.0.0.1', + ); + await sleep(5); + count += 1; + } + })(); + // We want to check that things function fine between bad data + const randomActivityProm = (async () => { + const stream = await client.connection.streamNew(); + await Promise.all([ + (async () => { + // Write data + const writer = stream.writable.getWriter(); + for (const message of messages) { + await writer.write(message); + await sleep(7); + } + await writer.close(); + })(), + (async () => { + // Consume readable + for await (const _ of stream.readable) { + // Do nothing + } + })(), + ]); + running = false; + })(); + // Wait for running activity to finish, should complete without error + await Promise.all([ + randomActivityProm, + serverStreamProms, + randomDataProm, + ]); + await client.destroy({ force: true }); + await server.stop(); + await socket.stop(); + }, + { numRuns: 1 }, + ); + testProp( + 'server handles random noise from external', + [ + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), + ], + async (data, messages) => { + const tlsConfig = await testsUtils.generateConfig('RSA'); + const socket = new QUICSocket({ + logger: logger.getChild('socket'), + }); + await socket.start({ + host: '127.0.0.1' as Host, + }); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + const connectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '127.0.0.1' as Host, + port: server.port, + localHost: '127.0.0.1' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const serverStreamProms: Array> = []; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable.pipeTo(stream.writable); + serverStreamProms.push(streamProm); + }, + ); + // Sending random data to client from the perspective of the server + let running = true; + const randomDataProm = (async () => { + let count = 0; + while (running) { + await socket.send( + data[count % data.length], + server.port, + '127.0.0.1', + ); + await sleep(5); + count += 1; + } + })(); + // We want to check that things function fine between bad data + const randomActivityProm = (async () => { + const stream = await client.connection.streamNew(); + await Promise.all([ + (async () => { + // Write data + const writer = stream.writable.getWriter(); + for (const message of messages) { + await writer.write(message); + await sleep(7); + } + await writer.close(); + })(), + (async () => { + // Consume readable + for await (const _ of stream.readable) { + // Do nothing + } + })(), + ]); + running = false; + })(); + // Wait for running activity to finish, should complete without error + await Promise.all([ + randomActivityProm, + serverStreamProms, + randomDataProm, + ]); + await client.destroy({ force: true }); + await server.stop(); + await socket.stop(); + }, + { numRuns: 1 }, + ); + }); + describe('keepalive', () => { + let tlsConfig: TLSConfigs; + beforeEach(async () => { + tlsConfig = await testsUtils.generateConfig('RSA'); + }); + test('connection can time out on client', async () => { + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + maxIdleTimeout: 1000, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + maxIdleTimeout: 100, + }, + }); + testsUtils.extractSocket(client, sockets); + // Setting no keepalive should cause the connection to time out + // It has cleaned up due to timeout + const clientConnection = client.connection; + const clientTimeoutProm = promise(); + clientConnection.addEventListener( + 'connectionError', + (event: events.QUICConnectionErrorEvent) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + clientTimeoutProm.resolveP(); + } + }, + ); + await clientTimeoutProm.p; + const serverConnection = await connectionEventProm.p; + await sleep(100); + // Server and client has cleaned up + expect(clientConnection[running]).toBeFalse(); + expect(serverConnection[running]).toBeFalse(); + + await client.destroy(); + await server.stop(); + }); + test('connection can time out on server', async () => { + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + maxIdleTimeout: 100, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + maxIdleTimeout: 1000, + }, + }); + testsUtils.extractSocket(client, sockets); + // Setting no keepalive should cause the connection to time out + // It has cleaned up due to timeout + const clientConnection = client.connection; + const serverConnection = await connectionEventProm.p; + const serverTimeoutProm = promise(); + serverConnection.addEventListener( + 'connectionError', + (event: events.QUICConnectionErrorEvent) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + serverTimeoutProm.resolveP(); + } + }, + ); + await serverTimeoutProm.p; + await sleep(100); + // Server and client has cleaned up + expect(clientConnection[running]).toBeFalse(); + expect(serverConnection[running]).toBeFalse(); + + await client.destroy(); + await server.stop(); + }); + test('keep alive prevents timeout on client', async () => { + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + maxIdleTimeout: 20000, + logKeys: './tmp/key1.log', + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + maxIdleTimeout: 100, + keepAliveIntervalTime: 50, + }, + }); + testsUtils.extractSocket(client, sockets); + // Setting no keepalive should cause the connection to time out + // It has cleaned up due to timeout + const clientConnection = client.connection; + const clientTimeoutProm = promise(); + clientConnection.addEventListener( + 'connectionError', + (event: events.QUICConnectionErrorEvent) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + clientTimeoutProm.resolveP(); + } + }, + ); + await connectionEventProm.p; + // Connection would timeout after 100ms if keep alive didn't work + await Promise.race([ + sleep(300), + clientTimeoutProm.p.then(() => { + throw Error('Connection timed out'); + }), + ]); + await client.destroy(); + await server.stop(); + }); + test('keep alive prevents timeout on server', async () => { + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + maxIdleTimeout: 100, + keepAliveIntervalTime: 50, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + maxIdleTimeout: 20000, + }, + }); + testsUtils.extractSocket(client, sockets); + // Setting no keepalive should cause the connection to time out + // It has cleaned up due to timeout + const serverConnection = await connectionEventProm.p; + const serverTimeoutProm = promise(); + serverConnection.addEventListener( + 'connectionError', + (event: events.QUICConnectionErrorEvent) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + serverTimeoutProm.resolveP(); + } + }, + ); + // Connection would time out after 100ms if keep alive didn't work + await Promise.race([ + sleep(300), + serverTimeoutProm.p.then(() => { + throw Error('Connection timed out'); + }), + ]); + await client.destroy(); + await server.stop(); + }); + test('client keep alive prevents timeout on server', async () => { + const connectionEventProm = promise(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + maxIdleTimeout: 100, + logKeys: './tmp/key1.log', + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => + connectionEventProm.resolveP(e.detail), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + maxIdleTimeout: 20000, + keepAliveIntervalTime: 50, + }, + }); + testsUtils.extractSocket(client, sockets); + // Setting no keepalive should cause the connection to time out + // It has cleaned up due to timeout + const serverConnection = await connectionEventProm.p; + const serverTimeoutProm = promise(); + serverConnection.addEventListener( + 'connectionError', + (event: events.QUICConnectionErrorEvent) => { + if (event.detail instanceof errors.ErrorQUICConnectionIdleTimeOut) { + serverTimeoutProm.resolveP(); + } + }, + ); + // Connection would time out after 100ms if keep alive didn't work + await Promise.race([ + sleep(300), + serverTimeoutProm.p.then(() => { + throw Error('Connection timed out'); + }), + ]); + await client.destroy(); + await server.stop(); + }); + test('Keep alive does not prevent connection timeout', async () => { + const clientProm = QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: 54444 as Port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + maxIdleTimeout: 100, + keepAliveIntervalTime: 50, + }, }); - // testProp( - // 'to ipv6 server succeeds', - // [tlsConfigWithCaArb], - // async (tlsConfigProm) => { - // const connectionEventProm = promise(); - // const tlsConfig = await tlsConfigProm; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // ...tlsConfigServer, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e), - // ); - // await server.start({ - // host: '::1' as Host, - // port: 0 as Port, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const conn = (await connectionEventProm.p).detail; - // expect(conn.localHost).toBe('::1'); - // expect(conn.localPort).toBe(server.port); - // expect(conn.remoteHost).toBe('::1'); - // expect(conn.remotePort).toBe(client.port); - // await client.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); - // testProp( - // 'to dual stack server succeeds', - // [tlsConfigWithCaArb], - // async (tlsConfigProm) => { - // const connectionEventProm = promise(); - // const tlsConfig = await tlsConfigProm; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // ...tlsConfigServer, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e), - // ); - // await server.start({ - // host: '::' as Host, - // port: 0 as Port, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::' as Host, // Will resolve to ::1 - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const conn = (await connectionEventProm.p).detail; - // expect(conn.localHost).toBe('::'); - // expect(conn.localPort).toBe(server.port); - // expect(conn.remoteHost).toBe('::1'); - // expect(conn.remotePort).toBe(client.port); - // await client.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); + await expect(clientProm).rejects.toThrow( + errors.ErrorQUICConnectionStartTimeOut, + ); + }); }); - // test('times out when there is no server', async () => { - // // QUICClient repeatedly dials until the connection timeout - // await expect( - // QUICClient.createQUICClient({ - // host: '127.0.0.1' as Host, - // port: 56666 as Port, - // localHost: '127.0.0.1' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // maxIdleTimeout: 1000, - // verifyPeer: false, - // }, - // }), - // ).rejects.toThrow(errors.ErrorQUICConnectionTimeout); - // }); - // test.todo('client times out after connection stops responding'); - // test.todo('server times out after connection stops responding'); - // test.todo('server handles socket error'); - // test.todo('client handles socket error'); - // describe('TLS rotation', () => { - // testProp( - // 'existing connections config is unchanged and still function', - // [tlsConfigWithCaArb, tlsConfigWithCaArb], - // async (tlsConfigProm1, tlsConfigProm2) => { - // const tlsConfig1 = await tlsConfigProm1; - // const tlsConfig2 = await tlsConfigProm2; - // fc.pre( - // JSON.stringify(tlsConfig1.tlsConfig) !== - // JSON.stringify(tlsConfig2.tlsConfig), - // ); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // ...tlsConfigServer, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client1 = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: true, - // verifyPem: tlsConfig1.ca.certChainPem, - // }, - // }); - // testsUtils.extractSocket(client1, sockets); - // const peerCertChainInitial = client1.connection.conn.peerCertChain(); - // server.updateConfig({ - // tlsConfig: tlsConfig2.tlsConfig, - // }); - // // The existing connection's certs should be unchanged - // const peerCertChainNew = client1.connection.conn.peerCertChain(); - // expect(peerCertChainNew![0].toString()).toStrictEqual( - // peerCertChainInitial![0].toString(), - // ); - // await client1.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); - // testProp( - // 'new connections use new config', - // [tlsConfigWithCaGENOKPArb, tlsConfigWithCaGENOKPArb], - // async (tlsConfigProm1, tlsConfigProm2) => { - // const tlsConfig1 = await tlsConfigProm1; - // const tlsConfig2 = await tlsConfigProm2; - // fc.pre( - // JSON.stringify(tlsConfig1.tlsConfig) !== - // JSON.stringify(tlsConfig2.tlsConfig), - // ); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfig1.tlsConfig, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client1 = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPem: tlsConfig1.ca.certChainPem, - // }, - // }); - // testsUtils.extractSocket(client1, sockets); - // const peerCertChainInitial = client1.connection.conn.peerCertChain(); - // server.updateConfig({ - // tlsConfig: tlsConfig2.tlsConfig, - // }); - // // Starting a new connection has a different peerCertChain - // const client2 = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: true, - // verifyPem: tlsConfig2.ca.certChainPem, - // }, - // }); - // testsUtils.extractSocket(client2, sockets); - // const peerCertChainNew = client2.connection.conn.peerCertChain(); - // expect(peerCertChainNew![0].toString()).not.toStrictEqual( - // peerCertChainInitial![0].toString(), - // ); - // await client1.destroy(); - // await client2.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); - // }); - // describe('graceful tls handshake', () => { - // testProp( - // 'server verification succeeds', - // [tlsConfigWithCaArb], - // async (tlsConfigsProm) => { - // const tlsConfigs = await tlsConfigsProm; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfigs.tlsConfig, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const handleConnectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // handleConnectionEventProm.resolveP, - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // Connection should succeed - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: true, - // verifyPem: tlsConfigs.ca.certChainPem, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // await handleConnectionEventProm.p; - // await client.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); - // // Fixme: client verification works regardless of certs - // testProp.skip( - // 'client verification succeeds', - // [tlsConfigWithCaArb, tlsConfigWithCaArb], - // async (tlsConfigProm1, tlsConfigProm2) => { - // const tlsConfigs1 = await tlsConfigProm1; - // const tlsConfigs2 = await tlsConfigProm2; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfigs1.tlsConfig, - // verifyPem: tlsConfigs2.ca.certChainPem, - // verifyPeer: true, - // }, - // }); - // const handleConnectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // handleConnectionEventProm.resolveP, - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // Connection should succeed - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // tlsConfig: tlsConfigs2.tlsConfig, - // verifyPeer: false, - // }, - // }); - // await client.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); - // testProp( - // 'client and server verification succeeds', - // [tlsConfigWithCaArb, tlsConfigWithCaArb], - // async (tlsConfigProm1, tlsConfigProm2) => { - // const tlsConfigs1 = await tlsConfigProm1; - // const tlsConfigs2 = await tlsConfigProm2; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfigs1.tlsConfig, - // verifyPem: tlsConfigs2.ca.certChainPem, - // verifyPeer: true, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const handleConnectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // handleConnectionEventProm.resolveP, - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // Connection should succeed - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // tlsConfig: tlsConfigs2.tlsConfig, - // verifyPem: tlsConfigs1.ca.certChainPem, - // verifyPeer: true, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // await handleConnectionEventProm.p; - // await client.destroy(); - // await server.stop(); - // }, - // { numRuns: 10 }, - // ); - // testProp( - // 'graceful failure verifying server', - // [tlsConfigWithCaArb], - // async (tlsConfigsProm) => { - // const tlsConfigs1 = await tlsConfigsProm; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfigs1.tlsConfig, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const handleConnectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // handleConnectionEventProm.resolveP, - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // Connection should succeed - // await expect( - // QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: true, - // }, - // }), - // ).toReject(); - // await handleConnectionEventProm.p; - // // Expect connection on the server to have ended - // // @ts-ignore: kidnap protected property - // // const connectionMap = server.connectionMap; - // // Expect(connectionMap.serverConnections.size).toBe(0); - // await server.stop(); - // }, - // { numRuns: 3 }, - // ); - // // Fixme: client verification works regardless of certs - // testProp.skip( - // 'graceful failure verifying client', - // [tlsConfigWithCaArb, tlsConfigWithCaArb], - // async (tlsConfigProm1, tlsConfigProm2) => { - // const tlsConfigs1 = await tlsConfigProm1; - // const tlsConfigs2 = await tlsConfigProm2; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfigs1.tlsConfig, - // verifyPeer: true, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const handleConnectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // handleConnectionEventProm.resolveP, - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // Connection should succeed - // await expect( - // QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // tlsConfig: tlsConfigs2.tlsConfig, - // verifyPeer: false, - // }, - // }), - // ).toReject(); - // await handleConnectionEventProm.p; - // // Expect connection on the server to have ended - // // @ts-ignore: kidnap protected property - // const connectionMap = server.connectionMap; - // expect(connectionMap.serverConnections.size).toBe(0); - // await server.stop(); - // }, - // { numRuns: 3 }, - // ); - // testProp( - // 'graceful failure verifying client and server', - // [tlsConfigWithCaArb, tlsConfigWithCaArb], - // async (tlsConfigProm1, tlsConfigProm2) => { - // const tlsConfigs1 = await tlsConfigProm1; - // const tlsConfigs2 = await tlsConfigProm2; - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: tlsConfigs1.tlsConfig, - // verifyPeer: true, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const handleConnectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // handleConnectionEventProm.resolveP, - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // Connection should succeed - // await expect( - // QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // tlsConfig: tlsConfigs2.tlsConfig, - // verifyPeer: true, - // }, - // }), - // ).toReject(); - // await handleConnectionEventProm.p; - // // Expect connection on the server to have ended - // // @ts-ignore: kidnap protected property - // // const connectionMap = server.connectionMap; - // // Expect(connectionMap.serverConnections.size).toBe(0); - // await server.stop(); - // }, - // { numRuns: 3 }, - // ); - // }); - // describe('UDP nat punching', () => { - // test('server can send init packets', async () => { - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // }); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // testsUtils.extractSocket(server, sockets); - // // @ts-ignore: kidnap protected property - // const socket = server.socket; - // const mockedSend = jest.spyOn(socket, 'send'); - // // The server can send packets - // // Should send 4 packets in 2 seconds - // const result = await server.initHolePunch( - // { - // host: '127.0.0.1' as Host, - // port: 52222 as Port, - // }, - // 2000, - // ); - // expect(mockedSend).toHaveBeenCalledTimes(4); - // expect(result).toBeFalse(); - // await server.stop(); - // }); - // test('init ends when connection establishes', async () => { - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // // The server can send packets - // // Should send 4 packets in 2 seconds - // const clientProm = sleep(1000) - // .then(async () => { - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // localPort: 55556 as Port, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // await client.destroy({ force: true }); - // }) - // .catch(() => {}); - // const result = await server.initHolePunch( - // { - // host: '127.0.0.1' as Host, - // port: 55556 as Port, - // }, - // 2000, - // ); - // await clientProm; - // expect(result).toBeTrue(); - // await server.stop(); - // }); - // test('init returns with existing connections', async () => { - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // localPort: 55556 as Port, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const result = await Promise.race([ - // server.initHolePunch( - // { - // host: '127.0.0.1' as Host, - // port: 55556 as Port, - // }, - // 2000, - // ), - // sleep(10).then(() => { - // throw Error('timed out'); - // }), - // ]); - // expect(result).toBeTrue(); - // await client.destroy({ force: true }); - // await server.stop(); - // }); - // }); - // describe('handles random packets', () => { - // testProp( - // 'client handles random noise from server', - // [ - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // ], - // async (data, messages) => { - // const socket = new QUICSocket({ - // logger: logger.getChild('socket'), - // }); - // await socket.start({ - // host: '127.0.0.1' as Host, - // }); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // socket, - // }); - // testsUtils.extractSocket(server, sockets); - // const connectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const conn = (await connectionEventProm.p).detail; - // // Do the test - // const serverStreamProms: Array> = []; - // conn.addEventListener( - // 'stream', - // (streamEvent: events.QUICConnectionStreamEvent) => { - // const stream = streamEvent.detail; - // const streamProm = stream.readable.pipeTo(stream.writable); - // serverStreamProms.push(streamProm); - // }, - // ); - // // Sending random data to client from the perspective of the server - // let running = true; - // const randomDataProm = (async () => { - // let count = 0; - // while (running) { - // await socket.send( - // data[count % data.length], - // client.port, - // '127.0.0.1', - // ); - // await sleep(5); - // count += 1; - // } - // })(); - // // We want to check that things function fine between bad data - // const randomActivityProm = (async () => { - // const stream = await client.connection.streamNew(); - // await Promise.all([ - // (async () => { - // // Write data - // const writer = stream.writable.getWriter(); - // for (const message of messages) { - // await writer.write(message); - // await sleep(7); - // } - // await writer.close(); - // })(), - // (async () => { - // // Consume readable - // for await (const _ of stream.readable) { - // // Do nothing - // } - // })(), - // ]); - // running = false; - // })(); - // // Wait for running activity to finish, should complete without error - // await Promise.all([ - // randomActivityProm, - // serverStreamProms, - // randomDataProm, - // ]); - // await client.destroy({ force: true }); - // await server.stop(); - // await socket.stop(); - // }, - // { numRuns: 1 }, - // ); - // testProp( - // 'client handles random noise from external', - // [ - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // ], - // async (data, messages) => { - // const socket = new QUICSocket({ - // logger: logger.getChild('socket'), - // }); - // await socket.start({ - // host: '127.0.0.1' as Host, - // }); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const connectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const conn = (await connectionEventProm.p).detail; - // // Do the test - // const serverStreamProms: Array> = []; - // conn.addEventListener( - // 'stream', - // (streamEvent: events.QUICConnectionStreamEvent) => { - // const stream = streamEvent.detail; - // const streamProm = stream.readable.pipeTo(stream.writable); - // serverStreamProms.push(streamProm); - // }, - // ); - // // Sending random data to client from the perspective of the server - // let running = true; - // const randomDataProm = (async () => { - // let count = 0; - // while (running) { - // await socket.send( - // data[count % data.length], - // client.port, - // '127.0.0.1', - // ); - // await sleep(5); - // count += 1; - // } - // })(); - // // We want to check that things function fine between bad data - // const randomActivityProm = (async () => { - // const stream = await client.connection.streamNew(); - // await Promise.all([ - // (async () => { - // // Write data - // const writer = stream.writable.getWriter(); - // for (const message of messages) { - // await writer.write(message); - // await sleep(7); - // } - // await writer.close(); - // })(), - // (async () => { - // // Consume readable - // for await (const _ of stream.readable) { - // // Do nothing - // } - // })(), - // ]); - // running = false; - // })(); - // // Wait for running activity to finish, should complete without error - // await Promise.all([ - // randomActivityProm, - // serverStreamProms, - // randomDataProm, - // ]); - // await client.destroy({ force: true }); - // await server.stop(); - // await socket.stop(); - // }, - // { numRuns: 1 }, - // ); - // testProp( - // 'server handles random noise from client', - // [ - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // ], - // async (data, messages) => { - // const socket = new QUICSocket({ - // logger: logger.getChild('socket'), - // }); - // await socket.start({ - // host: '127.0.0.1' as Host, - // }); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const connectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '127.0.0.1' as Host, - // port: server.port, - // socket, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const conn = (await connectionEventProm.p).detail; - // // Do the test - // const serverStreamProms: Array> = []; - // conn.addEventListener( - // 'stream', - // (streamEvent: events.QUICConnectionStreamEvent) => { - // const stream = streamEvent.detail; - // const streamProm = stream.readable.pipeTo(stream.writable); - // serverStreamProms.push(streamProm); - // }, - // ); - // // Sending random data to client from the perspective of the server - // let running = true; - // const randomDataProm = (async () => { - // let count = 0; - // while (running) { - // await socket.send( - // data[count % data.length], - // server.port, - // '127.0.0.1', - // ); - // await sleep(5); - // count += 1; - // } - // })(); - // // We want to check that things function fine between bad data - // const randomActivityProm = (async () => { - // const stream = await client.connection.streamNew(); - // await Promise.all([ - // (async () => { - // // Write data - // const writer = stream.writable.getWriter(); - // for (const message of messages) { - // await writer.write(message); - // await sleep(7); - // } - // await writer.close(); - // })(), - // (async () => { - // // Consume readable - // for await (const _ of stream.readable) { - // // Do nothing - // } - // })(), - // ]); - // running = false; - // })(); - // // Wait for running activity to finish, should complete without error - // await Promise.all([ - // randomActivityProm, - // serverStreamProms, - // randomDataProm, - // ]); - // await client.destroy({ force: true }); - // await server.stop(); - // await socket.stop(); - // }, - // { numRuns: 1 }, - // ); - // testProp( - // 'server handles random noise from external', - // [ - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // fc.array(fc.uint8Array({ minLength: 1 }), { minLength: 5 }).noShrink(), - // ], - // async (data, messages) => { - // const socket = new QUICSocket({ - // logger: logger.getChild('socket'), - // }); - // await socket.start({ - // host: '127.0.0.1' as Host, - // }); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig: fixtures.tlsConfigMemRSA1, - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // const connectionEventProm = promise(); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '127.0.0.1' as Host, - // port: server.port, - // localHost: '127.0.0.1' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // const conn = (await connectionEventProm.p).detail; - // // Do the test - // const serverStreamProms: Array> = []; - // conn.addEventListener( - // 'stream', - // (streamEvent: events.QUICConnectionStreamEvent) => { - // const stream = streamEvent.detail; - // const streamProm = stream.readable.pipeTo(stream.writable); - // serverStreamProms.push(streamProm); - // }, - // ); - // // Sending random data to client from the perspective of the server - // let running = true; - // const randomDataProm = (async () => { - // let count = 0; - // while (running) { - // await socket.send( - // data[count % data.length], - // server.port, - // '127.0.0.1', - // ); - // await sleep(5); - // count += 1; - // } - // })(); - // // We want to check that things function fine between bad data - // const randomActivityProm = (async () => { - // const stream = await client.connection.streamNew(); - // await Promise.all([ - // (async () => { - // // Write data - // const writer = stream.writable.getWriter(); - // for (const message of messages) { - // await writer.write(message); - // await sleep(7); - // } - // await writer.close(); - // })(), - // (async () => { - // // Consume readable - // for await (const _ of stream.readable) { - // // Do nothing - // } - // })(), - // ]); - // running = false; - // })(); - // // Wait for running activity to finish, should complete without error - // await Promise.all([ - // randomActivityProm, - // serverStreamProms, - // randomDataProm, - // ]); - // await client.destroy({ force: true }); - // await server.stop(); - // await socket.stop(); - // }, - // { numRuns: 1 }, - // ); - // }); - // describe('keepalive', () => { - // const tlsConfig = fixtures.tlsConfigMemRSA1; - // test('connection can time out on client', async () => { - // const connectionEventProm = promise(); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig, - // verifyPeer: false, - // maxIdleTimeout: 1000, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e.detail), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // maxIdleTimeout: 100, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // // Setting no keepalive should cause the connection to time out - // // It has cleaned up due to timeout - // const clientConnection = client.connection; - // const clientTimeoutProm = promise(); - // clientConnection.addEventListener( - // 'error', - // (event: events.QUICConnectionErrorEvent) => { - // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - // clientTimeoutProm.resolveP(); - // } - // }, - // ); - // await clientTimeoutProm.p; - // const serverConnection = await connectionEventProm.p; - // await sleep(100); - // // Server and client has cleaned up - // expect(clientConnection[destroyed]).toBeTrue(); - // expect(serverConnection[destroyed]).toBeTrue(); - // - // await client.destroy(); - // await server.stop(); - // }); - // test('connection can time out on server', async () => { - // const connectionEventProm = promise(); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig, - // verifyPeer: false, - // maxIdleTimeout: 100, - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e.detail), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // maxIdleTimeout: 1000, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // // Setting no keepalive should cause the connection to time out - // // It has cleaned up due to timeout - // const clientConnection = client.connection; - // const serverConnection = await connectionEventProm.p; - // const serverTimeoutProm = promise(); - // serverConnection.addEventListener( - // 'error', - // (event: events.QUICConnectionErrorEvent) => { - // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - // serverTimeoutProm.resolveP(); - // } - // }, - // ); - // await serverTimeoutProm.p; - // await sleep(100); - // // Server and client has cleaned up - // expect(clientConnection[destroyed]).toBeTrue(); - // expect(serverConnection[destroyed]).toBeTrue(); - // - // await client.destroy(); - // await server.stop(); - // }); - // test('keep alive prevents timeout on client', async () => { - // const connectionEventProm = promise(); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig, - // verifyPeer: false, - // maxIdleTimeout: 20000, - // logKeys: './tmp/key1.log', - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e.detail), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // maxIdleTimeout: 100, - // }, - // keepaliveIntervalTime: 50, - // }); - // testsUtils.extractSocket(client, sockets); - // // Setting no keepalive should cause the connection to time out - // // It has cleaned up due to timeout - // const clientConnection = client.connection; - // const clientTimeoutProm = promise(); - // clientConnection.addEventListener( - // 'error', - // (event: events.QUICConnectionErrorEvent) => { - // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - // clientTimeoutProm.resolveP(); - // } - // }, - // ); - // await connectionEventProm.p; - // // Connection would timeout after 100ms if keep alive didn't work - // await Promise.race([ - // sleep(300), - // clientTimeoutProm.p.then(() => { - // throw Error('Connection timed out'); - // }), - // ]); - // await client.destroy(); - // await server.stop(); - // }); - // test('keep alive prevents timeout on server', async () => { - // const connectionEventProm = promise(); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig, - // verifyPeer: false, - // maxIdleTimeout: 100, - // logKeys: './tmp/key1.log', - // }, - // keepaliveIntervalTime: 50, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e.detail), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // maxIdleTimeout: 20000, - // }, - // }); - // testsUtils.extractSocket(client, sockets); - // // Setting no keepalive should cause the connection to time out - // // It has cleaned up due to timeout - // const serverConnection = await connectionEventProm.p; - // const serverTimeoutProm = promise(); - // serverConnection.addEventListener( - // 'error', - // (event: events.QUICConnectionErrorEvent) => { - // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - // serverTimeoutProm.resolveP(); - // } - // }, - // ); - // // Connection would time out after 100ms if keep alive didn't work - // await Promise.race([ - // sleep(300), - // serverTimeoutProm.p.then(() => { - // throw Error('Connection timed out'); - // }), - // ]); - // await client.destroy(); - // await server.stop(); - // }); - // test('client keep alive prevents timeout on server', async () => { - // const connectionEventProm = promise(); - // const server = new QUICServer({ - // crypto: { - // key, - // ops: serverCrypto, - // }, - // logger: logger.getChild(QUICServer.name), - // config: { - // tlsConfig, - // verifyPeer: false, - // maxIdleTimeout: 100, - // logKeys: './tmp/key1.log', - // }, - // }); - // testsUtils.extractSocket(server, sockets); - // server.addEventListener( - // 'connection', - // (e: events.QUICServerConnectionEvent) => - // connectionEventProm.resolveP(e.detail), - // ); - // await server.start({ - // host: '127.0.0.1' as Host, - // }); - // const client = await QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: server.port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // maxIdleTimeout: 20000, - // }, - // keepaliveIntervalTime: 50, - // }); - // testsUtils.extractSocket(client, sockets); - // // Setting no keepalive should cause the connection to time out - // // It has cleaned up due to timeout - // const serverConnection = await connectionEventProm.p; - // const serverTimeoutProm = promise(); - // serverConnection.addEventListener( - // 'error', - // (event: events.QUICConnectionErrorEvent) => { - // if (event.detail instanceof errors.ErrorQUICConnectionTimeout) { - // serverTimeoutProm.resolveP(); - // } - // }, - // ); - // // Connection would time out after 100ms if keep alive didn't work - // await Promise.race([ - // sleep(300), - // serverTimeoutProm.p.then(() => { - // throw Error('Connection timed out'); - // }), - // ]); - // await client.destroy(); - // await server.stop(); - // }); - // test('Keep alive does not prevent connection timeout', async () => { - // const clientProm = QUICClient.createQUICClient({ - // host: '::ffff:127.0.0.1' as Host, - // port: 54444 as Port, - // localHost: '::' as Host, - // crypto: { - // ops: clientCrypto, - // }, - // logger: logger.getChild(QUICClient.name), - // config: { - // verifyPeer: false, - // maxIdleTimeout: 100, - // }, - // keepaliveIntervalTime: 50, - // }); - // await expect(clientProm).rejects.toThrow( - // errors.ErrorQUICConnectionTimeout, - // ); - // }); - // }); }); diff --git a/tests/tlsUtils.ts b/tests/tlsUtils.ts index 34d93ae6..adab0bbb 100644 --- a/tests/tlsUtils.ts +++ b/tests/tlsUtils.ts @@ -111,8 +111,6 @@ type KeyPair = { publicKey: Buffer; }; - - /** * Extracts Ed25519 Public Key from Ed25519 Private Key * The returned buffers are guaranteed to unpooled. @@ -155,7 +153,7 @@ const keyPairsArb = (min: number = 1, max?: number) => size: 'xsmall', }); -// const tlsConfigArb = (keyPairs: fc.Arbitrary> = keyPairsArb()) => +// Const tlsConfigArb = (keyPairs: fc.Arbitrary> = keyPairsArb()) => // keyPairs // .map(async (keyPairs) => await createTLSConfigWithChain(keyPairs)) // .noShrink(); @@ -207,7 +205,7 @@ export { publicKeyArb, keyPairArb, keyPairsArb, - // tlsConfigArb, + // TlsConfigArb, // tlsConfigWithCaArb, // tlsConfigWithCaRSAArb, // tlsConfigWithCaOKPArb, diff --git a/tests/utils.ts b/tests/utils.ts index 359c6e23..99174094 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -6,6 +6,7 @@ import type QUICServer from '@/QUICServer'; import type QUICStream from '@/QUICStream'; import { Crypto } from '@peculiar/webcrypto'; import * as x509 from '@peculiar/x509'; +import { never } from '@/utils'; /** * WebCrypto polyfill from @peculiar/webcrypto @@ -684,6 +685,65 @@ timeout: ${conn.timeout()}, `; } +type KeyTypes = 'RSA' | 'ECDSA' | 'ED25519'; +type TLSConfigs = { + key: string; + cert: string; + ca: string; +}; + +async function generateConfig(type: KeyTypes): Promise { + let privateKeyPem: string; + let keysLeaf: { publicKey: JsonWebKey; privateKey: JsonWebKey }; + let keysCa: { publicKey: JsonWebKey; privateKey: JsonWebKey }; + switch (type) { + case 'RSA': + { + keysLeaf = await generateKeyPairRSA(); + keysCa = await generateKeyPairRSA(); + privateKeyPem = (await keyPairRSAToPEM(keysLeaf)).privateKey; + } + break; + case 'ECDSA': + { + keysLeaf = await generateKeyPairECDSA(); + keysCa = await generateKeyPairECDSA(); + privateKeyPem = (await keyPairECDSAToPEM(keysLeaf)) + .privateKey; + } + break; + case 'ED25519': + { + keysLeaf = await generateKeyPairEd25519(); + keysCa = await generateKeyPairEd25519(); + privateKeyPem = (await keyPairEd25519ToPEM(keysLeaf)) + .privateKey; + } + break; + default: + never(); + } + + const certCa = await generateCertificate({ + certId: '0', + duration: 100000, + issuerPrivateKey: keysCa.privateKey, + subjectKeyPair: keysCa, + }); + const certLeaf = await generateCertificate({ + certId: '1', + duration: 100000, + issuerPrivateKey: keysCa.privateKey, + subjectKeyPair: keysLeaf, + }); + return { + key: privateKeyPem, + cert: certToPEM(certLeaf), + ca: certToPEM(certCa), + }; +} + + export { sleep, randomBytes, @@ -694,7 +754,7 @@ export { keyPairECDSAToPEM, keyPairEd25519ToPEM, generateCertificate, - // createTLSConfigWithChain, FIXME + // CreateTLSConfigWithChain, FIXME certToPEM, generateKeyHMAC, signHMAC, @@ -703,6 +763,7 @@ export { handleStreamProm, waitForTimeoutNull, connStats, + generateConfig, }; -export type { Messages, StreamData }; +export type { Messages, StreamData, KeyTypes, TLSConfigs }; From 110034df540582b8b6ba46863aab9e9605e651a5 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Tue, 27 Jun 2023 15:36:11 +1000 Subject: [PATCH 12/22] tests: creating native stream tests [ci skip] --- tests/QUICClient.test.ts | 4 +- tests/native/quiche.stream.lifecycle.test.ts | 474 +++++++++++++++++++ tests/utils.ts | 33 +- 3 files changed, 491 insertions(+), 20 deletions(-) create mode 100644 tests/native/quiche.stream.lifecycle.test.ts diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index f480dc0d..9f4ffa60 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -1,6 +1,7 @@ import type { ClientCrypto, Host, Port, ServerCrypto } from '@/types'; import type * as events from '@/events'; import type QUICConnection from '@/QUICConnection'; +import type { KeyTypes, TLSConfigs } from './utils'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; import { fc, testProp } from '@fast-check/jest'; import { running } from '@matrixai/async-init'; @@ -10,8 +11,7 @@ import QUICServer from '@/QUICServer'; import * as errors from '@/errors'; import { promise } from '@/utils'; import * as testsUtils from './utils'; -import { KeyTypes, sleep, TLSConfigs } from './utils'; - +import { sleep } from './utils'; describe(QUICClient.name, () => { const logger = new Logger(`${QUICClient.name} Test`, LogLevel.DEBUG, [ diff --git a/tests/native/quiche.stream.lifecycle.test.ts b/tests/native/quiche.stream.lifecycle.test.ts new file mode 100644 index 00000000..43d9f7fa --- /dev/null +++ b/tests/native/quiche.stream.lifecycle.test.ts @@ -0,0 +1,474 @@ +import type { Connection, StreamIter } from '@/native'; +import type { ClientCrypto, Host, Port, ServerCrypto } from '@'; +import { Host as HostPort, quiche } from '@/native'; +import QUICConnectionId from '@/QUICConnectionId'; +import { QUICConfig } from '@'; +import { buildQuicheConfig, clientDefault, serverDefault } from '@/config'; +import * as utils from '@/utils'; +import * as testsUtils from '../utils'; + +function sendPacket( + connectionSource: Connection, + connectionDestination: Connection, +) { + const dataBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const [serverSendLength, sendInfo] = connectionSource.send(dataBuffer); + connectionDestination.recv(dataBuffer.subarray(0, serverSendLength), { + to: sendInfo.to, + from: sendInfo.from, + }); +} + +function iterToArray(iter: StreamIter) { + const array: Array = []; + for (const iterElement of iter) { + array.push(iterElement); + } + return array; +} + +/** + * Does all the steps for initiating a stream on both sides. + * Used as a starting point for a bunch of tests. + */ +function initStreamState( + connectionSource: Connection, + connectionDestination: Connection, + streamId: number, +) { + const message = Buffer.from('Message'); + connectionSource.streamSend(0, message, false); + sendPacket(connectionSource, connectionDestination); + + throw Error('TMP IMP'); +} + +describe('quiche stream lifecycle', () => { + const localHost = '127.0.0.1' as Host; + const clientHost = { + host: localHost, + port: 55555 as Port, + }; + const serverHost = { + host: localHost, + port: 55556, + }; + + let crypto: { + key: ArrayBuffer; + ops: ClientCrypto & ServerCrypto; + }; + + let clientConn: Connection; + let serverConn: Connection; + + beforeAll(async () => { + crypto = { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + randomBytes: testsUtils.randomBytes, + }, + }; + }); + + describe('with RSA certs', () => { + const setupConnectionsRSA = async () => { + const clientConfig = buildQuicheConfig({ + ...clientDefault, + verifyPeer: false, + }); + const tlsConfigServer = await testsUtils.generateConfig('RSA'); + const serverConfig = buildQuicheConfig({ + ...serverDefault, + + key: tlsConfigServer.key, + cert: tlsConfigServer.cert, + }); + + // Randomly genrate the client SCID + const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + await crypto.ops.randomBytes(scidBuffer); + const clientScid = new QUICConnectionId(scidBuffer); + clientConn = quiche.Connection.connect( + null, + clientScid, + clientHost, + serverHost, + clientConfig, + ); + + const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + let [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const clientHeaderInitial = quiche.Header.fromSlice( + clientBuffer.subarray(0, clientSendLength), + quiche.MAX_CONN_ID_LEN, + ); + const clientDcid = new QUICConnectionId(clientHeaderInitial.dcid); + + // Derives a new SCID by signing the client's generated DCID + // This is only used during the stateless retry + const serverScid = new QUICConnectionId( + await crypto.ops.sign(crypto.key, clientDcid), + 0, + quiche.MAX_CONN_ID_LEN, + ); + // Stateless retry + const token = await utils.mintToken(clientDcid, clientHost.host, crypto); + const retryDatagram = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); + const retryDatagramLength = quiche.retry( + clientScid, + clientDcid, + serverScid, + token, + clientHeaderInitial.version, + retryDatagram, + ); + + // Retry gets sent back to be processed by the client + clientConn.recv(retryDatagram.subarray(0, retryDatagramLength), { + to: clientHost, + from: serverHost, + }); + + // Client will retry the initial packet with the token + [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + + // Server accept + serverConn = quiche.Connection.accept( + serverScid, + clientDcid, + serverHost, + clientHost, + serverConfig, + ); + // Server receives the retried initial frame + serverConn.recv(clientBuffer.subarray(0, clientSendLength), { + to: serverHost, + from: clientHost, + }); + + // Client <-initial- server + sendPacket(serverConn, clientConn); + // Client -initial-> server + sendPacket(clientConn, serverConn); + // Client <-handshake- server + sendPacket(serverConn, clientConn); + // Client -handshake-> server + sendPacket(clientConn, serverConn); + // Client <-short- server + sendPacket(serverConn, clientConn); + // Client -short-> server + sendPacket(clientConn, serverConn); + // Both are established + }; + + describe('stream can be created', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(setupConnectionsRSA); + + test('initializing stream with 0-len message', () => { + clientConn.streamSend(0, new Uint8Array(0), false); + // No data is sent + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).toContain(0); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('Server state does not exist yet', () => { + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('first stream message creates server state', async () => { + const message = Buffer.from('Message'); + expect(clientConn.streamSend(0, message, false)).toEqual( + message.byteLength, + ); + + // Packet should be sent + sendPacket(clientConn, serverConn); + + // Server state for stream is created + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading the message + + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(message.byteLength); + expect(fin).toBe(false); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + message.toString(), + ); + + // State is updated after reading + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Ack is returned + sendPacket(serverConn, clientConn); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('reverse data can be sent', () => { + // Server state before sending + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + const serverStreamCapacity = serverConn.streamCapacity(0); + expect(serverStreamCapacity).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state before sending + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Sending data + const message = Buffer.from('Message 2'); + serverConn.streamSend(0, message, false); + + // Server state is updated + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeLessThan(serverStreamCapacity); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Packet is sent + sendPacket(serverConn, clientConn); + + // Client state after sending + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Read message + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(message.byteLength); + expect(fin).toBe(false); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + message.toString(), + ); + + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + + // Ack returned + sendPacket(clientConn, serverConn); + + // Server state is updated + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + // Capacity has increased again + expect(serverConn.streamCapacity(0)).toEqual(serverStreamCapacity); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('closing forward stream with fin frame', async () => { + clientConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Is finished + expect(serverConn.streamFinished(0)).toBeTrue(); + // Still readable + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('closing reverse stream with fin frame', async () => { + serverConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Is finished + expect(clientConn.streamFinished(0)).toBeTrue(); + // Still readable + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Server sends ack back + sendPacket(clientConn, serverConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + }); +}); + +// TODO: +// Stream only finishes after reading out data. +// test permutations of force closing sending/receiving, by client or server, with and without buffered data. diff --git a/tests/utils.ts b/tests/utils.ts index 99174094..25be4097 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -698,27 +698,25 @@ async function generateConfig(type: KeyTypes): Promise { let keysCa: { publicKey: JsonWebKey; privateKey: JsonWebKey }; switch (type) { case 'RSA': - { - keysLeaf = await generateKeyPairRSA(); - keysCa = await generateKeyPairRSA(); - privateKeyPem = (await keyPairRSAToPEM(keysLeaf)).privateKey; - } + { + keysLeaf = await generateKeyPairRSA(); + keysCa = await generateKeyPairRSA(); + privateKeyPem = (await keyPairRSAToPEM(keysLeaf)).privateKey; + } break; case 'ECDSA': - { - keysLeaf = await generateKeyPairECDSA(); - keysCa = await generateKeyPairECDSA(); - privateKeyPem = (await keyPairECDSAToPEM(keysLeaf)) - .privateKey; - } + { + keysLeaf = await generateKeyPairECDSA(); + keysCa = await generateKeyPairECDSA(); + privateKeyPem = (await keyPairECDSAToPEM(keysLeaf)).privateKey; + } break; case 'ED25519': - { - keysLeaf = await generateKeyPairEd25519(); - keysCa = await generateKeyPairEd25519(); - privateKeyPem = (await keyPairEd25519ToPEM(keysLeaf)) - .privateKey; - } + { + keysLeaf = await generateKeyPairEd25519(); + keysCa = await generateKeyPairEd25519(); + privateKeyPem = (await keyPairEd25519ToPEM(keysLeaf)).privateKey; + } break; default: never(); @@ -743,7 +741,6 @@ async function generateConfig(type: KeyTypes): Promise { }; } - export { sleep, randomBytes, From b1ea509b39650b56848618b2ecb72b242ac4fe25 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Tue, 27 Jun 2023 16:35:54 +1000 Subject: [PATCH 13/22] fix: small fixes [ci skip] --- src/QUICClient.ts | 7 ++- src/QUICConnection.ts | 107 +++++++++++++++++++++------------------ src/QUICServer.ts | 1 + tests/QUICClient.test.ts | 2 +- 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index addfb5ab..6f5d900e 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -72,6 +72,7 @@ class QUICClient extends EventTarget { * @param opts.reasonToCode - optional reason to code map * @param opts.codeToReason - optional code to reason map * @param opts.logger - optional logger + * @param ctx */ public static createQUICClient( opts: { @@ -216,9 +217,10 @@ class QUICClient extends EventTarget { ), }); const abortController = new AbortController(); - ctx.signal.addEventListener('abort', (r) => { + const abortHandler = (r) => { abortController.abort(r); - }); + }; + ctx.signal.addEventListener('abort', abortHandler); try { await Promise.race([ connection.start({ ...ctx, signal: abortController.signal }), @@ -234,6 +236,7 @@ class QUICClient extends EventTarget { throw e; } finally { socket.removeEventListener('socketError', handleQUICSocketError); + ctx.signal.removeEventListener('abort', abortHandler); } address = utils.buildAddress(host_, port); const client = new this({ diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 09525666..96dca20a 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -10,6 +10,7 @@ import type { StreamCodeToReason, StreamId, StreamReasonToCode, + VerifyCallback, } from './types'; import type { Connection, ConnectionErrorCode, SendInfo } from './native/types'; import { Lock, LockBox, Monitor, RWLockWriter } from '@matrixai/async-locks'; @@ -30,9 +31,6 @@ import * as utils from './utils'; import { never } from './utils'; import * as errors from './errors'; -// FIXME -type VerifyCallback = (certs: Array) => void; - /** * Think of this as equivalent to `net.Socket`. * Errors here are emitted to the connection only. @@ -361,58 +359,63 @@ class QUICConnection extends EventTarget { public async start(@context ctx: ContextTimed): Promise { this.logger.info(`Start ${this.constructor.name}`); ctx.signal.throwIfAborted(); - ctx.signal.addEventListener('abort', (r) => { + const abortHandler = (r) => { this.rejectEstablishedP(r); this.rejectSecureEstablishedP(r); // Is this actually true? // Technically the connection is closed this.rejectClosedP(r); - }); + }; + ctx.signal.addEventListener('abort', abortHandler); // Set the connection up this.socket.connectionMap.set(this.connectionId, this); // Waits for the first short packet after establishment // This ensures that TLS has been established and verified on both sides await this.send(); - await this.secureEstablishedP.catch((e) => { - this.socket.connectionMap.delete(this.connectionId); + await this.secureEstablishedP + .catch((e) => { + this.socket.connectionMap.delete(this.connectionId); - if (this.conn.isTimedOut()) { - // We don't dispatch an event here, it was already done in the timeout. - throw new errors.ErrorQUICConnectionStartTimeOut(); - } + if (this.conn.isTimedOut()) { + // We don't dispatch an event here, it was already done in the timeout. + throw new errors.ErrorQUICConnectionStartTimeOut(); + } - // Emit error if local error - const localError = this.conn.localError(); - if (localError != null) { - const message = `connection start failed with localError ${Buffer.from( - localError.reason, - ).toString()}(${localError.errorCode})`; - this.logger.info(message); - throw new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...localError, - }, - }); - } - // Emit error if peer error - const peerError = this.conn.peerError(); - if (peerError != null) { - const message = `Connection start failed with peerError ${Buffer.from( - peerError.reason, - ).toString()}(${peerError.errorCode})`; - this.logger.info(message); - throw new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...peerError, - }, - }); - } - // Throw the default error if none of the above were true, this shouldn't really happen - throw e; - }); + // Emit error if local error + const localError = this.conn.localError(); + if (localError != null) { + const message = `connection start failed with localError ${Buffer.from( + localError.reason, + ).toString()}(${localError.errorCode})`; + this.logger.info(message); + throw new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...localError, + }, + }); + } + // Emit error if peer error + const peerError = this.conn.peerError(); + if (peerError != null) { + const message = `Connection start failed with peerError ${Buffer.from( + peerError.reason, + ).toString()}(${peerError.errorCode})`; + this.logger.info(message); + throw new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...peerError, + }, + }); + } + // Throw the default error if none of the above were true, this shouldn't really happen + throw e; + }) + .finally(() => { + ctx.signal.removeEventListener('abort', abortHandler); + }); this.logger.warn('secured'); // After this is done // We need to established the keep alive interval time @@ -937,7 +940,7 @@ class QUICConnection extends EventTarget { const timeout = this.conn.timeout(); // If this is `null`, then technically there's nothing to do if (timeout == null) return; - // Allow an extra 1ms for the delay to fully complete so we can avoid a repeated 0ms delay + // Allow an extra 1ms for the delay to fully complete, so we can avoid a repeated 0ms delay this.connTimeOutTimer = new Timer({ delay: timeout + 1, handler: connTimeOutHandler, @@ -948,14 +951,18 @@ class QUICConnection extends EventTarget { // If this is `null` there's nothing to do if (timeout == null) return; // If there was an existing timer, we cancel it and set a new one - if (this.connTimeOutTimer != null) { - this.connTimeOutTimer.cancel(); + if ( + this.connTimeOutTimer != null && + this.connTimeOutTimer.status === null + ) { + this.connTimeOutTimer.reset(timeout); + } else { + this.logger.debug(`timeout created with delay ${timeout}`); + this.connTimeOutTimer = new Timer({ + delay: timeout + 1, + handler: connTimeOutHandler, + }); } - this.logger.debug(`timeout created with delay ${timeout}`); - this.connTimeOutTimer = new Timer({ - delay: timeout, - handler: connTimeOutHandler, - }); } /** diff --git a/src/QUICServer.ts b/src/QUICServer.ts index 1489f96a..daf62889 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -350,6 +350,7 @@ class QUICServer extends EventTarget { await connection.start(); // TODO: pass ctx } catch (e) { // Ignoring any errors here as a failure to connect + // FIXME: should we emit a connection error here? return; } this.dispatchEvent( diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 9f4ffa60..224187b7 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -14,7 +14,7 @@ import * as testsUtils from './utils'; import { sleep } from './utils'; describe(QUICClient.name, () => { - const logger = new Logger(`${QUICClient.name} Test`, LogLevel.DEBUG, [ + const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), From 6009eb2badac96c7f78d5746d0f36d09a1a4d090 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Wed, 28 Jun 2023 16:30:29 +1000 Subject: [PATCH 14/22] feat: `QUICConnection` and `QUICClient` handles `ctx` and timeouts [ci skip] --- src/QUICClient.ts | 45 ++++---- src/QUICConnection.ts | 228 ++++++++++++++++++++++++--------------- src/QUICServer.ts | 5 +- src/config.ts | 4 +- tests/QUICClient.test.ts | 53 ++++++++- 5 files changed, 216 insertions(+), 119 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 6f5d900e..3320eada 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -200,32 +200,32 @@ class QUICClient extends EventTarget { `Cannot connect to ${host_} an IPv4 mapped IPv6 QUICClient`, ); } - const connection = new QUICConnection({ - type: 'client', - scid, - socket, - remoteInfo: { - host: host_, - port, - }, - config: quicConfig, - reasonToCode, - codeToReason, - verifyCallback, - logger: logger.getChild( - `${QUICConnection.name} ${scid.toString().slice(32)}`, - ), - }); const abortController = new AbortController(); - const abortHandler = (r) => { - abortController.abort(r); + const abortHandler = () => { + abortController.abort(ctx.signal.reason); }; ctx.signal.addEventListener('abort', abortHandler); + const connectionProm = QUICConnection.createQUICConnection( + { + type: 'client', + scid, + socket, + remoteInfo: { + host: host_, + port, + }, + config: quicConfig, + reasonToCode, + codeToReason, + verifyCallback, + logger: logger.getChild( + `${QUICConnection.name} ${scid.toString().slice(32)}`, + ), + }, + ctx, + ); try { - await Promise.race([ - connection.start({ ...ctx, signal: abortController.signal }), - socketErrorP, - ]); + await Promise.race([connectionProm, socketErrorP]); } catch (e) { // In case the `connection.start` is on-going, we need to abort it abortController.abort(e); @@ -238,6 +238,7 @@ class QUICClient extends EventTarget { socket.removeEventListener('socketError', handleQUICSocketError); ctx.signal.removeEventListener('abort', abortHandler); } + const connection = await connectionProm; address = utils.buildAddress(host_, port); const client = new this({ socket, diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 96dca20a..9985e1f4 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -28,7 +28,7 @@ import QUICStream from './QUICStream'; import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; -import { never } from './utils'; +import { never, promise } from './utils'; import * as errors from './errors'; /** @@ -207,6 +207,103 @@ class QUICConnection extends EventTarget { protected count = 0; protected verifyCallback: VerifyCallback | undefined; + public static createQUICConnection( + args: + | { + type: 'client'; + scid: QUICConnectionId; + dcid?: undefined; + remoteInfo: RemoteInfo; + config: QUICConfig; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; + logger?: Logger; + } + | { + type: 'server'; + scid: QUICConnectionId; + dcid: QUICConnectionId; + remoteInfo: RemoteInfo; + config: QUICConfig; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; + logger?: Logger; + }, + ctx?: Partial, + ): PromiseCancellable; + @timedCancellable(true, Infinity, errors.ErrorQUICConnectionStartTimeOut) + public static async createQUICConnection( + args: + | { + type: 'client'; + scid: QUICConnectionId; + dcid?: undefined; + remoteInfo: RemoteInfo; + config: QUICConfig; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; + logger?: Logger; + } + | { + type: 'server'; + scid: QUICConnectionId; + dcid: QUICConnectionId; + remoteInfo: RemoteInfo; + config: QUICConfig; + socket: QUICSocket; + reasonToCode?: StreamReasonToCode; + codeToReason?: StreamCodeToReason; + verifyCallback?: VerifyCallback; + logger?: Logger; + }, + @context ctx: ContextTimed, + ): Promise { + ctx.signal.throwIfAborted(); + const abortProm = promise(); + const abortHandler = () => { + abortProm.rejectP(ctx.signal.reason); + }; + ctx.signal.addEventListener('abort', abortHandler); + const connection = new this(args); + // This ensures that TLS has been established and verified on both sides + try { + await Promise.race([ + Promise.all([ + connection.start(), + connection.establishedP, + connection.secureEstablishedP, + ]), + abortProm.p, + ]); + } catch (e) { + await connection.stop({ + applicationError: false, + errorCode: 42, // FIXME: use a proper code + errorMessage: e.message, + force: true, + }); + throw e; + } finally { + ctx.signal.removeEventListener('abort', abortHandler); + } + connection.logger.warn('secured'); + // After this is done + // We need to establish the keep alive interval time + if (connection.config.keepAliveIntervalTime != null) { + connection.startKeepAliveIntervalTimer( + connection.config.keepAliveIntervalTime, + ); + } + + return connection; + } + public constructor({ type, scid, @@ -351,79 +448,13 @@ class QUICConnection extends EventTarget { } /** - * This is the same as basically waiting for `secureEstablishedP` - * While this is occurring one can call the `recv` and `send` to make this happen + * This will set up the connection initiate sending */ - public start(ctx?: Partial): PromiseCancellable; - @timedCancellable(true, Infinity, errors.ErrorQUICConnectionStartTimeOut) - public async start(@context ctx: ContextTimed): Promise { + public async start(): Promise { this.logger.info(`Start ${this.constructor.name}`); - ctx.signal.throwIfAborted(); - const abortHandler = (r) => { - this.rejectEstablishedP(r); - this.rejectSecureEstablishedP(r); - - // Is this actually true? - // Technically the connection is closed - this.rejectClosedP(r); - }; - ctx.signal.addEventListener('abort', abortHandler); // Set the connection up this.socket.connectionMap.set(this.connectionId, this); - // Waits for the first short packet after establishment - // This ensures that TLS has been established and verified on both sides await this.send(); - await this.secureEstablishedP - .catch((e) => { - this.socket.connectionMap.delete(this.connectionId); - - if (this.conn.isTimedOut()) { - // We don't dispatch an event here, it was already done in the timeout. - throw new errors.ErrorQUICConnectionStartTimeOut(); - } - - // Emit error if local error - const localError = this.conn.localError(); - if (localError != null) { - const message = `connection start failed with localError ${Buffer.from( - localError.reason, - ).toString()}(${localError.errorCode})`; - this.logger.info(message); - throw new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...localError, - }, - }); - } - // Emit error if peer error - const peerError = this.conn.peerError(); - if (peerError != null) { - const message = `Connection start failed with peerError ${Buffer.from( - peerError.reason, - ).toString()}(${peerError.errorCode})`; - this.logger.info(message); - throw new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...peerError, - }, - }); - } - // Throw the default error if none of the above were true, this shouldn't really happen - throw e; - }) - .finally(() => { - ctx.signal.removeEventListener('abort', abortHandler); - }); - this.logger.warn('secured'); - // After this is done - // We need to established the keep alive interval time - if (this.config.keepAliveIntervalTime != null) { - this.startKeepAliveIntervalTimer(this.config.keepAliveIntervalTime); - } - // Do we remove the on abort event listener? - // I forgot... this.logger.info(`Started ${this.constructor.name}`); } @@ -472,7 +503,8 @@ class QUICConnection extends EventTarget { this.stopKeepAliveIntervalTimer(); try { mon = mon ?? new Monitor(this.lockbox, RWLockWriter); - await mon.withF(this.lockCode, async (mon) => { + // Trigger closing connection in the background and await close later. + void mon.withF(this.lockCode, async (mon) => { // If this is already closed, then `Done` will be thrown // Otherwise it can send `CONNECTION_CLOSE` frame // This can be 0x1c close at the QUIC layer or no errors @@ -481,10 +513,19 @@ class QUICConnection extends EventTarget { // 1 packet containing a `CONNECTION_CLOSE` frame too // (with `NO_ERROR` code if appropriate) // It must enter into a draining state, and no other packets can be sent - this.conn.close(applicationError, errorCode, Buffer.from(errorMessage)); - // If we get a `Done` exception we don't bother calling send - // The send only gets sent if the `Done` is not the case - await this.send(mon); + try { + this.conn.close( + applicationError, + errorCode, + Buffer.from(errorMessage), + ); + // If we get a `Done` exception we don't bother calling send + // The send only gets sent if the `Done` is not the case + await this.send(mon); + } catch (e) { + // Ignore 'Done' if already closed + if (e.message !== 'Done') throw e; + } }); } catch (e) { // If the connection is already closed, `Done` will be thrown @@ -507,9 +548,15 @@ class QUICConnection extends EventTarget { this.socket.connectionMap.delete(this.connectionId); if (this.conn.isTimedOut()) { + const error = this.secured + ? new errors.ErrorQUICConnectionIdleTimeOut() + : new errors.ErrorQUICConnectionStartTimeOut(); + + this.rejectEstablishedP(error); + this.rejectSecureEstablishedP(error); this.dispatchEvent( new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionIdleTimeOut(), + detail: error, }), ); } @@ -521,14 +568,17 @@ class QUICConnection extends EventTarget { peerError.reason, ).toString()}(${peerError.errorCode})`; this.logger.info(message); + const error = new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...peerError, + }, + }); + this.rejectEstablishedP(error); + this.rejectSecureEstablishedP(error); this.dispatchEvent( new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...peerError, - }, - }), + detail: error, }), ); } @@ -539,14 +589,17 @@ class QUICConnection extends EventTarget { localError.reason, ).toString()}(${localError.errorCode})`; this.logger.info(message); + const error = new errors.ErrorQUICConnectionInternal(message, { + data: { + type: 'local', + ...localError, + }, + }); + this.rejectEstablishedP(error); + this.rejectSecureEstablishedP(error); this.dispatchEvent( new events.QUICConnectionErrorEvent({ - detail: new errors.ErrorQUICConnectionInternal(message, { - data: { - type: 'local', - ...localError, - }, - }), + detail: error, }), ); } @@ -864,7 +917,6 @@ class QUICConnection extends EventTarget { // Then we just have to proceed! // Plus if we are called here this.resolveClosedP(); - await this.stop( this.conn.localError() ?? this.conn.peerError() ?? {}, mon, diff --git a/src/QUICServer.ts b/src/QUICServer.ts index daf62889..ddd11152 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -332,7 +332,7 @@ class QUICServer extends EventTarget { `Accepting new connection from QUIC packet from ${remoteInfo.host}:${remoteInfo.port}`, ); const clientConnRef = Buffer.from(header.scid).toString('hex').slice(32); - const connection = new QUICConnection({ + const connectionProm = QUICConnection.createQUICConnection({ type: 'server', scid: newScid, dcid: dcidOriginal, @@ -347,12 +347,13 @@ class QUICServer extends EventTarget { ), }); try { - await connection.start(); // TODO: pass ctx + await connectionProm; // TODO: pass ctx } catch (e) { // Ignoring any errors here as a failure to connect // FIXME: should we emit a connection error here? return; } + const connection = await connectionProm; this.dispatchEvent( new events.QUICServerConnectionEvent({ detail: connection }), ); diff --git a/src/config.ts b/src/config.ts index ae61a30e..c3ec8510 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,7 +28,7 @@ const clientDefault: QUICConfig = { verifyPeer: true, verifyAllowFail: false, grease: true, - maxIdleTimeout: 0, + maxIdleTimeout: 1 * 60 * 1000, maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 maxSendUdpPayloadSize: quiche.MIN_CLIENT_INITIAL_LEN, // 1200, initialMaxData: 10 * 1024 * 1024, @@ -48,7 +48,7 @@ const serverDefault: QUICConfig = { verifyPeer: false, verifyAllowFail: false, grease: true, - maxIdleTimeout: 0, + maxIdleTimeout: 1 * 60 * 1000, maxRecvUdpPayloadSize: quiche.MAX_DATAGRAM_SIZE, // 65527 maxSendUdpPayloadSize: quiche.MIN_CLIENT_INITIAL_LEN, // 1200 initialMaxData: 10 * 1024 * 1024, diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 224187b7..1f62c558 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -5,6 +5,7 @@ import type { KeyTypes, TLSConfigs } from './utils'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; import { fc, testProp } from '@fast-check/jest'; import { running } from '@matrixai/async-init'; +import { Timer } from '@matrixai/timer'; import QUICSocket from '@/QUICSocket'; import QUICClient from '@/QUICClient'; import QUICServer from '@/QUICServer'; @@ -14,7 +15,7 @@ import * as testsUtils from './utils'; import { sleep } from './utils'; describe(QUICClient.name, () => { - const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ + const logger = new Logger(`${QUICClient.name} Test`, LogLevel.INFO, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -208,10 +209,52 @@ describe(QUICClient.name, () => { }), ).rejects.toThrow(errors.ErrorQUICConnectionStartTimeOut); }); - test.todo('client times out with ctx timer while starting'); - test.todo('client aborted while starting'); - test.todo('client times out after connection stops responding'); - test.todo('server times out after connection stops responding'); + test('client times out with ctx timer while starting', async () => { + // QUICClient repeatedly dials until the connection timeout + await expect( + QUICClient.createQUICClient( + { + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + // Prevent `maxIdleTimeout` timeout + maxIdleTimeout: 100000, + verifyPeer: false, + }, + }, + { timer: new Timer({ delay: 100 }) }, + ), + ).rejects.toThrow(errors.ErrorQUICClientCreateTimeOut); + }); + test('client times out with ctx signal while starting', async () => { + // QUICClient repeatedly dials until the connection timeout + const abortController = new AbortController(); + const clientProm = QUICClient.createQUICClient( + { + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + // Prevent `maxIdleTimeout` timeout + maxIdleTimeout: 100000, + verifyPeer: false, + }, + }, + { signal: abortController.signal }, + ); + await sleep(100); + abortController.abort(Error('abort error')); + await expect(clientProm).rejects.toThrow(Error('abort error')); + }); test.todo('server handles socket error'); test.todo('client handles socket error'); }); From cea873bc6491affa215c4f61e93e8c4d3f9673b6 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Wed, 28 Jun 2023 17:36:20 +1000 Subject: [PATCH 15/22] tests: adding custom TLS verification tests [ci skip] --- src/QUICConnection.ts | 60 ++++----- src/QUICServer.ts | 9 +- tests/QUICClient.test.ts | 261 +++++++++++++++++++++++++++++++++++---- 3 files changed, 270 insertions(+), 60 deletions(-) diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 9985e1f4..f03b54c8 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -682,7 +682,6 @@ class QUICConnection extends EventTarget { // If short frame if (header.ty === 5) { this.shortReceived = true; - this.conn.sendAckEliciting(); } } @@ -845,34 +844,39 @@ class QUICConnection extends EventTarget { sendInfo.to.host, ); this.logger.debug(`sent ${sendLength} bytes`); - } - // Handling custom TLS verification, this must be done after the following conditions. - // 1. Connection established. - // 2. Certs available. - // 3. Sent after connection has established. - if ( - !this.customVerified && - this.conn.isEstablished() && - this.conn.peerCertChain() != null - ) { - this.customVerified = true; - const peerCerts = this.conn.peerCertChain(); - if (peerCerts == null) never(); - const peerCertsPem = peerCerts.map((c) => utils.certificateDERToPEM(c)); - // Dispatching certs available event - // this.dispatchEvent(new events.QUICConnectionRemoteCertEvent()); TODO - try { - if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); - this.conn.sendAckEliciting(); - } catch (e) { - // Force the connection to end. - // Error 304 indicates cert chain failed verification. - // Error 372 indicates cert chain was missing. - this.conn.close( - false, - 304, - Buffer.from(`Custom TLSFail: ${e.message}`), + + // Handling custom TLS verification, this must be done after the following conditions. + // 1. Connection established. + // 2. Certs available. + // 3. Sent after connection has established. + if ( + !this.customVerified && + this.conn.isEstablished() && + this.conn.peerCertChain() != null + ) { + this.customVerified = true; + const peerCerts = this.conn.peerCertChain(); + if (peerCerts == null) never(); + const peerCertsPem = peerCerts.map((c) => + utils.certificateDERToPEM(c), ); + try { + if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); + this.logger.warn('TLS verification succeeded'); + this.conn.sendAckEliciting(); + } catch (e) { + // Force the connection to end. + // Error 304 indicates cert chain failed verification. + // Error 372 indicates cert chain was missing. + this.logger.warn( + `TLS fail due to [${e.message}], closing connection`, + ); + this.conn.close( + false, + 304, + Buffer.from(`Custom TLSFail: ${e.message}`), + ); + } } } diff --git a/src/QUICServer.ts b/src/QUICServer.ts index ddd11152..ff9a62f6 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -84,14 +84,7 @@ class QUICServer extends EventTarget { }: { crypto: { key: ArrayBuffer; - ops: { - sign(key: ArrayBuffer, data: ArrayBuffer): Promise; - verify( - key: ArrayBuffer, - data: ArrayBuffer, - sig: ArrayBuffer, - ): Promise; - }; + ops: ServerCrypto; }; config: Partial & { key: string | Array | Uint8Array | Array; diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 1f62c558..de9ca713 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -12,10 +12,10 @@ import QUICServer from '@/QUICServer'; import * as errors from '@/errors'; import { promise } from '@/utils'; import * as testsUtils from './utils'; -import { sleep } from './utils'; +import { generateConfig, sleep } from './utils'; describe(QUICClient.name, () => { - const logger = new Logger(`${QUICClient.name} Test`, LogLevel.INFO, [ + const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -32,8 +32,8 @@ describe(QUICClient.name, () => { }; let sockets: Set; - const defaultType = 'RSA'; const types: Array = ['RSA', 'ECDSA', 'ED25519']; + const defaultType = types[0]; // We need to test the stream making beforeEach(async () => { @@ -72,7 +72,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -84,7 +84,6 @@ describe(QUICClient.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - logKeys: './tmp/key.log', }, }); testsUtils.extractSocket(client, sockets); @@ -131,7 +130,6 @@ describe(QUICClient.name, () => { logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, - logKeys: './tmp/key.log', }, }); testsUtils.extractSocket(client, sockets); @@ -623,7 +621,7 @@ describe(QUICClient.name, () => { logger: logger.getChild('socket'), }); await socket.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const server = new QUICServer({ crypto: { @@ -646,7 +644,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -732,7 +730,7 @@ describe(QUICClient.name, () => { logger: logger.getChild('socket'), }); await socket.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const server = new QUICServer({ crypto: { @@ -754,7 +752,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -840,7 +838,7 @@ describe(QUICClient.name, () => { logger: logger.getChild('socket'), }); await socket.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const server = new QUICServer({ crypto: { @@ -862,10 +860,10 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, + host: localhost, port: server.port, socket, crypto: { @@ -948,7 +946,7 @@ describe(QUICClient.name, () => { logger: logger.getChild('socket'), }); await socket.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const server = new QUICServer({ crypto: { @@ -970,12 +968,12 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '127.0.0.1' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -1072,7 +1070,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e.detail), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -1132,7 +1130,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e.detail), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -1183,7 +1181,6 @@ describe(QUICClient.name, () => { cert: tlsConfig.cert, verifyPeer: false, maxIdleTimeout: 20000, - logKeys: './tmp/key1.log', }, }); testsUtils.extractSocket(server, sockets); @@ -1193,7 +1190,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e.detail), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -1256,7 +1253,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e.detail), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -1307,7 +1304,6 @@ describe(QUICClient.name, () => { cert: tlsConfig.cert, verifyPeer: false, maxIdleTimeout: 100, - logKeys: './tmp/key1.log', }, }); testsUtils.extractSocket(server, sockets); @@ -1317,7 +1313,7 @@ describe(QUICClient.name, () => { connectionEventProm.resolveP(e.detail), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ host: '::ffff:127.0.0.1' as Host, @@ -1376,4 +1372,221 @@ describe(QUICClient.name, () => { ); }); }); + describe.each(types)('custom TLS verification with %s', (type) => { + test('server succeeds custom verification', async () => { + const tlsConfigs = await generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs.key, + cert: tlsConfigs.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + handleConnectionEventProm.resolveP, + ); + await server.start({ + host: localhost, + }); + // Connection should succeed + const verifyProm = promise | undefined>(); + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + verifyAllowFail: true, + }, + verifyCallback: (certs) => { + verifyProm.resolveP(certs); + }, + }); + testsUtils.extractSocket(client, sockets); + await handleConnectionEventProm.p; + await expect(verifyProm.p).toResolve(); + await client.destroy(); + await server.stop(); + }); + test('server fails custom verification', async () => { + const tlsConfigs = await generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs.key, + cert: tlsConfigs.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + (event: events.QUICServerConnectionEvent) => + handleConnectionEventProm.resolveP(event.detail), + ); + await server.start({ + host: localhost, + }); + // Connection should fail + const clientProm = QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: true, + verifyAllowFail: true, + }, + verifyCallback: () => { + throw Error('SOME ERROR'); + }, + }); + clientProm.catch(() => {}); + + // Server connection is never emitted + await Promise.race([ + handleConnectionEventProm.p.then(() => { + throw Error('Server connection should not be emitted'); + }), + // Allow some time + sleep(200), + ]); + + await expect(clientProm).rejects.toThrow( + errors.ErrorQUICConnectionInternal, + ); + + await server.stop(); + }); + test('client succeeds custom verification', async () => { + const tlsConfigs = await generateConfig(type); + const verifyProm = promise | undefined>(); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs.key, + cert: tlsConfigs.cert, + verifyPeer: true, + verifyAllowFail: true, + }, + verifyCallback: (certs) => { + verifyProm.resolveP(certs); + }, + }); + testsUtils.extractSocket(server, sockets); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + handleConnectionEventProm.resolveP, + ); + await server.start({ + host: localhost, + }); + // Connection should succeed + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + key: tlsConfigs.key, + cert: tlsConfigs.cert, + }, + }); + testsUtils.extractSocket(client, sockets); + await handleConnectionEventProm.p; + await expect(verifyProm.p).toResolve(); + await client.destroy(); + await server.stop(); + }); + test('client fails custom verification', async () => { + const tlsConfigs = await generateConfig(type); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfigs.key, + cert: tlsConfigs.cert, + verifyPeer: true, + verifyAllowFail: true, + }, + verifyCallback: () => { + throw Error('SOME ERROR'); + }, + }); + testsUtils.extractSocket(server, sockets); + const handleConnectionEventProm = promise(); + server.addEventListener( + 'serverConnection', + (event: events.QUICServerConnectionEvent) => + handleConnectionEventProm.resolveP(event.detail), + ); + await server.start({ + host: localhost, + port: 55555 as Port, + }); + // Connection should fail + const clientProm = QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + key: tlsConfigs.key, + cert: tlsConfigs.cert, + verifyPeer: false, + }, + }); + clientProm.catch(() => {}); + + // Server connection is never emitted + await Promise.race([ + handleConnectionEventProm.p.then(() => { + throw Error('Server connection should not be emitted'); + }), + // Allow some time + sleep(200), + ]); + + await expect(clientProm).rejects.toThrow( + errors.ErrorQUICConnectionInternal, + ); + + await server.stop(); + }); + }); }); From 968ae89624827a20dc9befa0001d07f374f3dea9 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Jun 2023 12:03:25 +1000 Subject: [PATCH 16/22] feat: applying `maxIdleTimeout` constraints for keep-alive and start timeout [ci skip] --- src/QUICConnection.ts | 22 ++++++-- src/errors.ts | 5 ++ tests/QUICClient.test.ts | 108 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index f03b54c8..335aa0d9 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -264,6 +264,12 @@ class QUICConnection extends EventTarget { }, @context ctx: ContextTimed, ): Promise { + const timeoutTime = ctx.timer.getTimeout(); + if (timeoutTime !== Infinity && timeoutTime >= args.config.maxIdleTimeout) { + throw new errors.ErrorQUICConnectionInvalidConfig( + 'connection timeout timer must be strictly less than maxIdleTimeout', + ); + } ctx.signal.throwIfAborted(); const abortProm = promise(); const abortHandler = () => { @@ -292,7 +298,7 @@ class QUICConnection extends EventTarget { } finally { ctx.signal.removeEventListener('abort', abortHandler); } - connection.logger.warn('secured'); + connection.logger.debug('secureEstablishedP'); // After this is done // We need to establish the keep alive interval time if (connection.config.keepAliveIntervalTime != null) { @@ -342,6 +348,16 @@ class QUICConnection extends EventTarget { }) { super(); this.logger = logger ?? new Logger(`${this.constructor.name} ${scid}`); + // Checking constraints + if ( + config.keepAliveIntervalTime != null && + config.keepAliveIntervalTime >= config.maxIdleTimeout + ) { + throw new errors.ErrorQUICConnectionInvalidConfig( + 'keepAliveIntervalTime must be shorter than maxIdleTimeout', + ); + } + const quicheConfig = buildQuicheConfig(config); let conn: Connection; if (type === 'client') { @@ -862,13 +878,13 @@ class QUICConnection extends EventTarget { ); try { if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); - this.logger.warn('TLS verification succeeded'); + this.logger.debug('TLS verification succeeded'); this.conn.sendAckEliciting(); } catch (e) { // Force the connection to end. // Error 304 indicates cert chain failed verification. // Error 372 indicates cert chain was missing. - this.logger.warn( + this.logger.debug( `TLS fail due to [${e.message}], closing connection`, ); this.conn.close( diff --git a/src/errors.ts b/src/errors.ts index 6ede7054..579ef510 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -112,6 +112,10 @@ class ErrorQUICConnectionInternal extends ErrorQUICConnection { } & POJO; } +class ErrorQUICConnectionInvalidConfig extends ErrorQUICConnection { + static description = 'QUIC connection invalid configuration'; +} + class ErrorQUICStream extends ErrorQUIC { static description = 'QUIC Stream error'; } @@ -163,6 +167,7 @@ export { ErrorQUICConnectionStartTimeOut, ErrorQUICConnectionIdleTimeOut, ErrorQUICConnectionInternal, + ErrorQUICConnectionInvalidConfig, ErrorQUICStream, ErrorQUICStreamDestroyed, ErrorQUICStreamLocked, diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index de9ca713..51e41eb2 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -207,6 +207,58 @@ describe(QUICClient.name, () => { }), ).rejects.toThrow(errors.ErrorQUICConnectionStartTimeOut); }); + test('intervalTimeoutTime must be less than maxIdleTimeout', async () => { + // Larger keepAliveIntervalTime throws + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + keepAliveIntervalTime: 1000, + verifyPeer: false, + }, + }), + ).rejects.toThrow(errors.ErrorQUICConnectionInvalidConfig); + // Smaller keepAliveIntervalTime doesn't cause a problem + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + keepAliveIntervalTime: 100, + verifyPeer: false, + }, + }), + ).rejects.not.toThrow(errors.ErrorQUICConnectionInvalidConfig); + // Not setting an interval doesn't cause a problem either + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + verifyPeer: false, + }, + }), + ).rejects.not.toThrow(errors.ErrorQUICConnectionInvalidConfig); + }); test('client times out with ctx timer while starting', async () => { // QUICClient repeatedly dials until the connection timeout await expect( @@ -229,6 +281,62 @@ describe(QUICClient.name, () => { ), ).rejects.toThrow(errors.ErrorQUICClientCreateTimeOut); }); + test('ctx timer must be less than maxIdleTimeout', async () => { + // Larger timer throws + await expect( + QUICClient.createQUICClient( + { + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + verifyPeer: false, + }, + }, + { timer: new Timer({ delay: 1000 }) }, + ), + ).rejects.toThrow(errors.ErrorQUICConnectionInvalidConfig); + // Smaller keepAliveIntervalTime doesn't cause a problem + await expect( + QUICClient.createQUICClient( + { + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + verifyPeer: false, + }, + }, + { timer: new Timer({ delay: 100 }) }, + ), + ).rejects.not.toThrow(errors.ErrorQUICConnectionInvalidConfig); + // Not setting an interval doesn't cause a problem either + await expect( + QUICClient.createQUICClient({ + host: localhost, + port: 56666 as Port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + maxIdleTimeout: 200, + verifyPeer: false, + }, + }), + ).rejects.not.toThrow(errors.ErrorQUICConnectionInvalidConfig); + }); test('client times out with ctx signal while starting', async () => { // QUICClient repeatedly dials until the connection timeout const abortController = new AbortController(); From 85b0375954cc53eda626795fda1723dee7ad13cd Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Jun 2023 12:25:39 +1000 Subject: [PATCH 17/22] tests: fixing up tests to match changes - fixed up `QUICServer.test.ts` - fixed up `QUICSocket.test.ts` - fixed up `concurrency.test.ts` [ci skip] --- tests/QUICServer.test.ts | 254 +++++++++++++++++++++----------------- tests/QUICSocket.test.ts | 29 +---- tests/concurrency.test.ts | 113 ++++++++++------- 3 files changed, 215 insertions(+), 181 deletions(-) diff --git a/tests/QUICServer.test.ts b/tests/QUICServer.test.ts index 814bdaf4..726cf5e2 100644 --- a/tests/QUICServer.test.ts +++ b/tests/QUICServer.test.ts @@ -1,5 +1,12 @@ import type { X509Certificate } from '@peculiar/x509'; -import type { QUICConfig, Crypto, Host, Hostname, Port } from '@/types'; +import type { + QUICConfig, + Host, + Hostname, + Port, + ClientCrypto, + ServerCrypto, +} from '@/types'; import dgram from 'dgram'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; import QUICServer from '@/QUICServer'; @@ -77,24 +84,26 @@ describe(QUICServer.name, () => { certEd25519PEM = testsUtils.certToPEM(certEd25519); }); // This has to be setup asynchronously due to key generation - let crypto: { - key: ArrayBuffer; - ops: Crypto; - }; + let clientCrypto: ClientCrypto; + let serverCrypto: ServerCrypto; + let key: ArrayBuffer; beforeEach(async () => { - crypto = { - key: await testsUtils.generateKeyHMAC(), - ops: { - sign: testsUtils.signHMAC, - verify: testsUtils.verifyHMAC, - randomBytes: testsUtils.randomBytes, - }, + key = await testsUtils.generateKeyHMAC(); + clientCrypto = { + randomBytes: testsUtils.randomBytes, + }; + serverCrypto = { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, }; }); describe('start and stop', () => { test('with RSA', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, @@ -109,7 +118,10 @@ describe(QUICServer.name, () => { }); test('with ECDSA', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, @@ -124,7 +136,10 @@ describe(QUICServer.name, () => { }); test('with Ed25519', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -141,7 +156,10 @@ describe(QUICServer.name, () => { describe('binding to host and port', () => { test('listen on IPv4', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -157,7 +175,10 @@ describe(QUICServer.name, () => { }); test('listen on IPv6', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -173,7 +194,10 @@ describe(QUICServer.name, () => { }); test('listen on dual stack', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -191,7 +215,10 @@ describe(QUICServer.name, () => { // NOT RECOMMENDED, because send addresses will have to be mapped // addresses, which means you can ONLY connect to mapped addresses const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -214,7 +241,10 @@ describe(QUICServer.name, () => { }); test('listen on hostname', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -232,7 +262,10 @@ describe(QUICServer.name, () => { }); test('listen on hostname and custom resolver', async () => { const quicServer = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, config: { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, @@ -248,83 +281,84 @@ describe(QUICServer.name, () => { await quicServer.stop(); }); }); - describe.only('connection bootstrap', () => { - // Test without peer verification - test.only('', async () => { - const quicServer = new QUICServer({ - crypto, - config: { - // Key: keyPairRSAPEM.privateKey, - // cert: certRSAPEM, - key: keyPairECDSAPEM.privateKey, - cert: certECDSAPEM, - verifyPeer: false, - }, - logger: logger.getChild('QUICServer'), - }); - await quicServer.start({ - host: '127.0.0.1' as Host, - }); - - const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); - await crypto.ops.randomBytes(scidBuffer); - const scid = new QUICConnectionId(scidBuffer); - - // Verify peer - // Note that you cannot send to IPv4 from dual stack socket - // It must be sent as IPv4 mapped IPv6 - - const socket = new QUICSocket({ - crypto, - logger: logger.getChild(QUICSocket.name), - }); - await socket.start({ - host: '127.0.0.1' as Host, - }); - - // ??? - const clientConfig: QUICConfig = { - ...clientDefault, - verifyPeer: false, - }; - - // This creates a connection state - // We now need to trigger it - const connection = await QUICConnection.connectQUICConnection({ - scid, - socket, - remoteInfo: { - host: quicServer.host, - port: quicServer.port, - }, - config: clientConfig, - logger: logger.getChild(QUICConnection.name), - }); - - connection.addEventListener('error', (e) => { - console.log('error', e); - }); - - // Trigger the connection - await connection.send(); - - // Wait till it is established - console.log('BEFORE ESTABLISHED P'); - await connection.establishedP; - console.log('AFTER ESTABLISHED P'); - - // You must destroy the connection - console.log('DESTROY CONNECTION'); - await connection.destroy(); - console.log('DESTROYED CONNECTION'); - - console.log('STOP SOCKET'); - await socket.stop(); - console.time('STOPPED SOCKET'); - await quicServer.stop(); - console.timeEnd('STOPPED SOCKET'); - }); - }); + // Describe.only('connection bootstrap', () => { + // // Test without peer verification + // test.only('', async () => { + // const quicServer = new QUICServer({ + // crypto: { + // key, + // ops: serverCrypto, + // }, + // config: { + // key: keyPairECDSAPEM.privateKey, + // cert: certECDSAPEM, + // verifyPeer: false, + // }, + // logger: logger.getChild('QUICServer'), + // }); + // await quicServer.start({ + // host: '127.0.0.1' as Host, + // }); + // + // const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); + // await clientCrypto.randomBytes(scidBuffer); + // const scid = new QUICConnectionId(scidBuffer); + // + // // Verify peer + // // Note that you cannot send to IPv4 from dual stack socket + // // It must be sent as IPv4 mapped IPv6 + // + // const socket = new QUICSocket({ + // logger: logger.getChild(QUICSocket.name), + // }); + // await socket.start({ + // host: '127.0.0.1' as Host, + // }); + // + // // ??? + // const clientConfig: QUICConfig = { + // ...clientDefault, + // verifyPeer: false, + // }; + // + // // This creates a connection state + // // We now need to trigger it + // const connection = await QUICConnection.createQUICConnection({ + // type: 'client', + // scid, + // socket, + // remoteInfo: { + // host: quicServer.host, + // port: quicServer.port, + // }, + // config: clientConfig, + // logger: logger.getChild(QUICConnection.name), + // }); + // + // connection.addEventListener('error', (e) => { + // console.log('error', e); + // }); + // + // // Trigger the connection + // await connection.send(); + // + // // Wait till it is established + // console.log('BEFORE ESTABLISHED P'); + // await connection.establishedP; + // console.log('AFTER ESTABLISHED P'); + // + // // You must destroy the connection + // console.log('DESTROY CONNECTION'); + // await connection.stop(); + // console.log('DESTROYED CONNECTION'); + // + // console.log('STOP SOCKET'); + // await socket.stop(); + // console.time('STOPPED SOCKET'); + // await quicServer.stop(); + // console.timeEnd('STOPPED SOCKET'); + // }); + // }); // Test('bootstrapping a new connection', async () => { // const quicServer = new QUICServer({ // crypto, @@ -335,50 +369,50 @@ describe(QUICServer.name, () => { // logger: logger.getChild('QUICServer'), // }); // await quicServer.start(); - + // // const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); // await crypto.ops.randomBytes(scidBuffer); // const scid = new QUICConnectionId(scidBuffer); - + // // const socket = new QUICSocket({ // crypto, // resolveHostname: utils.resolveHostname, // logger: logger.getChild(QUICSocket.name), // }); // await socket.start(); - + // // // Const config = buildQuicheConfig({ // // ...clientDefault // // }); // // Here we want to VERIFY the peer // // If we use the same certificate // // then it should be consider as if it is trusted! - + // // const quicConfig: QUICConfig = { // ...clientDefault, // verifyPeer: true, // }; - + // // const connection = await QUICConnection.connectQUICConnection({ // scid, // socket, - + // // remoteInfo: { // host: utils.resolvesZeroIP(quicServer.host), // port: quicServer.port, // }, - + // // config: quicConfig, // }); - + // // await socket.stop(); // await quicServer.stop(); - + // // // We can run with several rsa keypairs and certificates // }); - describe('updating configuration', () => { - // We want to test changing the configuration over time - }); + // describe('updating configuration', () => { + // // We want to test changing the configuration over time + // }); // Test hole punching, there's an initiation function // We can make it start doing this, but technically it's the socket's duty to do this // not just the server side diff --git a/tests/QUICSocket.test.ts b/tests/QUICSocket.test.ts index 006e5823..a59ef96a 100644 --- a/tests/QUICSocket.test.ts +++ b/tests/QUICSocket.test.ts @@ -1,4 +1,4 @@ -import type { Crypto, Host } from '@/types'; +import type { Host } from '@/types'; import type QUICConnection from '@/QUICConnection'; import dgram from 'dgram'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; @@ -6,17 +6,12 @@ import QUICSocket from '@/QUICSocket'; import * as utils from '@/utils'; import * as errors from '@/errors'; import QUICConnectionId from '@/QUICConnectionId'; -import * as testsUtils from './utils'; describe(QUICSocket.name, () => { const logger = new Logger(`${QUICSocket.name} Test`, LogLevel.WARN, [ new StreamHandler(), ]); // This has to be setup asynchronously due to key generation - let crypto: { - key: ArrayBuffer; - ops: Crypto; - }; let ipv4Socket: dgram.Socket; let ipv6Socket: dgram.Socket; let dualStackSocket: dgram.Socket; @@ -63,14 +58,6 @@ describe(QUICSocket.name, () => { dualStackSocketMessageResolveP = resolveP; }; beforeEach(async () => { - crypto = { - key: await testsUtils.generateKey(), - ops: { - sign: testsUtils.sign, - verify: testsUtils.verify, - randomBytes: testsUtils.randomBytes, - }, - }; ipv4Socket = dgram.createSocket({ type: 'udp4', }); @@ -117,7 +104,6 @@ describe(QUICSocket.name, () => { }); describe('ipv4', () => { const socket = new QUICSocket({ - crypto, logger, }); const msg = Buffer.from('Hello World'); @@ -164,7 +150,6 @@ describe(QUICSocket.name, () => { }); describe('ipv6', () => { const socket = new QUICSocket({ - crypto, logger, }); const msg = Buffer.from('Hello World'); @@ -219,7 +204,6 @@ describe(QUICSocket.name, () => { }); describe('ipv6 only when using `::`', () => { const socket = new QUICSocket({ - crypto, logger, }); const msg = Buffer.from('Hello World'); @@ -270,7 +254,6 @@ describe(QUICSocket.name, () => { }); describe('dual stack', () => { const socket = new QUICSocket({ - crypto, logger, }); const msg = Buffer.from('Hello World'); @@ -342,7 +325,6 @@ describe(QUICSocket.name, () => { }); test('enabling ipv6 only prevents binding to ipv4 hosts', async () => { const socket = new QUICSocket({ - crypto, logger, }); await expect( @@ -355,7 +337,6 @@ describe(QUICSocket.name, () => { }); test('disabling ipv6 only does not prevent binding to ipv6 hosts', async () => { const socket = new QUICSocket({ - crypto, logger, }); await socket.start({ @@ -366,7 +347,6 @@ describe(QUICSocket.name, () => { }); test('ipv4 wildcard to ipv4 succeeds', async () => { const socket = new QUICSocket({ - crypto, logger, }); await socket.start({ @@ -387,7 +367,6 @@ describe(QUICSocket.name, () => { }); test('ipv6 wildcard to ipv6 succeeds', async () => { const socket = new QUICSocket({ - crypto, logger, }); await socket.start({ @@ -408,7 +387,6 @@ describe(QUICSocket.name, () => { }); describe('ipv4 mapped ipv6 - dotted decimal variant', () => { const socket = new QUICSocket({ - crypto, logger, }); const msg = Buffer.from('Hello World'); @@ -458,7 +436,6 @@ describe(QUICSocket.name, () => { }); describe('ipv4 mapped ipv6 - hex variant', () => { const socket = new QUICSocket({ - crypto, logger, }); const msg = Buffer.from('Hello World'); @@ -510,7 +487,6 @@ describe(QUICSocket.name, () => { }); test('socket should throw if stopped with active connections', async () => { const socket = new QUICSocket({ - crypto, logger, }); await socket.start({ @@ -530,7 +506,6 @@ describe(QUICSocket.name, () => { }); test('socket should stop when forced with active connections', async () => { const socket = new QUICSocket({ - crypto, logger, }); await socket.start({ @@ -542,6 +517,6 @@ describe(QUICSocket.name, () => { socket.connectionMap.set(connectionId, { type: 'client', } as QUICConnection); - await expect(socket.stop(true)).toResolve(); + await expect(socket.stop({ force: true })).toResolve(); }); }); diff --git a/tests/concurrency.test.ts b/tests/concurrency.test.ts index feae82a5..1a57f802 100644 --- a/tests/concurrency.test.ts +++ b/tests/concurrency.test.ts @@ -1,18 +1,21 @@ import type * as events from '@/events'; -import type { Crypto, Host, Port, StreamReasonToCode } from '@'; -import type { TlsConfig } from '@/config'; -import type { QUICConfig } from '@/config'; +import type { + ClientCrypto, + Host, + Port, + ServerCrypto, + StreamReasonToCode, +} from '@'; import type { Messages, StreamData } from './utils'; +import type { QUICConfig } from '@'; import { fc, testProp } from '@fast-check/jest'; import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; import QUICServer from '@/QUICServer'; import { promise } from '@/utils'; import QUICClient from '@/QUICClient'; import QUICSocket from '@/QUICSocket'; -import { tlsConfigWithCaArb } from './tlsUtils'; -import { handleStreamProm, sleep } from './utils'; +import { generateConfig, handleStreamProm, sleep } from './utils'; import * as testsUtils from './utils'; -import * as certFixtures from './fixtures/certFixtures'; describe('Concurrency tests', () => { const logger = new Logger(`${QUICClient.name} Test`, LogLevel.WARN, [ @@ -21,10 +24,9 @@ describe('Concurrency tests', () => { ), ]); // This has to be setup asynchronously due to key generation - let crypto: { - key: ArrayBuffer; - ops: Crypto; - }; + let key: ArrayBuffer; + let clientCrypto: ClientCrypto; + let serverCrypto: ServerCrypto; // Tracking resources let sockets: Array; @@ -36,13 +38,13 @@ describe('Concurrency tests', () => { }; beforeEach(async () => { - crypto = { - key: await testsUtils.generateKey(), - ops: { - sign: testsUtils.sign, - verify: testsUtils.verify, - randomBytes: testsUtils.randomBytes, - }, + key = await testsUtils.generateKeyHMAC(); + clientCrypto = { + randomBytes: testsUtils.randomBytes, + }; + serverCrypto = { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, }; sockets = []; }); @@ -51,7 +53,7 @@ describe('Concurrency tests', () => { logger.info('AFTER EACH'); const stopProms: Array> = []; for (const socket of sockets) { - stopProms.push(socket.stop(true)); + stopProms.push(socket.stop({ force: true })); } await Promise.allSettled(stopProms); }); @@ -124,16 +126,20 @@ describe('Concurrency tests', () => { testProp( 'Multiple clients connecting to a server', - [tlsConfigWithCaArb, connectionsArb, streamsArb(3)], - async (tlsConfigProm, clientDatas, serverStreams) => { - const tlsConfig = await tlsConfigProm; + [connectionsArb, streamsArb(3)], + async (clientDatas, serverStreams) => { + const tlsConfig = await generateConfig('RSA'); const cleanUpHoldProm = promise(); const serverProm = (async () => { const server = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, logger: logger.getChild(QUICServer.name), config: { - tlsConfig: tlsConfig.tlsConfig, + key: tlsConfig.key, + cert: tlsConfig.cert, verifyPeer: false, }, }); @@ -159,7 +165,7 @@ describe('Concurrency tests', () => { await cleanUpHoldProm.p; await Promise.all(serverStreamProms); } finally { - await conn.destroy({ force: true }); + await conn.stop({ force: true }); logger.info( `server conn result ${JSON.stringify( await Promise.allSettled(serverStreamProms), @@ -199,7 +205,9 @@ describe('Concurrency tests', () => { host: '::ffff:127.0.0.1' as Host, port: socketPort1, localHost: '::' as Host, - crypto, + crypto: { + ops: clientCrypto, + }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, @@ -240,16 +248,20 @@ describe('Concurrency tests', () => { ); testProp( 'Multiple clients sharing a socket', - [tlsConfigWithCaArb, connectionsArb, streamsArb(3)], - async (tlsConfigProm, clientDatas, serverStreams) => { - const tlsConfig = await tlsConfigProm; + [connectionsArb, streamsArb(3)], + async (clientDatas, serverStreams) => { + const tlsConfig = await generateConfig('RSA'); const cleanUpHoldProm = promise(); const serverProm = (async () => { const server = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, logger: logger.getChild(QUICServer.name), config: { - tlsConfig: tlsConfig.tlsConfig, + key: tlsConfig.key, + cert: tlsConfig.cert, verifyPeer: false, }, }); @@ -275,7 +287,7 @@ describe('Concurrency tests', () => { await cleanUpHoldProm.p; await Promise.all(serverStreamProms); } finally { - await conn.destroy({ force: true }); + await conn.stop({ force: true }); logger.info( `server conn result ${JSON.stringify( await Promise.allSettled(serverStreamProms), @@ -305,7 +317,6 @@ describe('Concurrency tests', () => { })(); // Creating socket const socket = new QUICSocket({ - crypto, logger: logger.getChild('socket'), }); await socket.start({ @@ -323,7 +334,9 @@ describe('Concurrency tests', () => { host: '127.0.0.1' as Host, port: socketPort1, socket, - crypto, + crypto: { + ops: clientCrypto, + }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, @@ -374,12 +387,18 @@ describe('Concurrency tests', () => { socket: QUICSocket | undefined; port: Port | undefined; cleanUpHoldProm: Promise; - config: Partial & { tlsConfig: TlsConfig }; + config: Partial & { + key: string | Array | Uint8Array | Array; + cert: string | Array | Uint8Array | Array; + }; serverStreams: Array; reasonToCode: StreamReasonToCode; }) => { const server = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, socket, logger: logger.getChild(QUICServer.name), config, @@ -410,7 +429,7 @@ describe('Concurrency tests', () => { await cleanUpHoldProm; await Promise.all(serverStreamProms); } finally { - await conn.destroy({ force: true }); + await conn.stop({ force: true }); logger.info( `server conn result ${JSON.stringify( await Promise.allSettled(serverStreamProms), @@ -440,6 +459,8 @@ describe('Concurrency tests', () => { 'Multiple clients sharing a socket with a server', [connectionsArb, connectionsArb, streamsArb(3), streamsArb(3)], async (clientDatas1, clientDatas2, serverStreams1, serverStreams2) => { + const tlsConfig1 = await generateConfig('RSA'); + const tlsConfig2 = await generateConfig('RSA'); const clientsInfosA = clientDatas1.map((v) => v.streams.length); const clientsInfosB = clientDatas2.map((v) => v.streams.length); logger.info(`clientsA: ${clientsInfosA}`); @@ -447,11 +468,9 @@ describe('Concurrency tests', () => { const cleanUpHoldProm = promise(); // Creating socket const socket1 = new QUICSocket({ - crypto, logger: logger.getChild('socket'), }); const socket2 = new QUICSocket({ - crypto, logger: logger.getChild('socket'), }); sockets.push(socket1); @@ -469,7 +488,8 @@ describe('Concurrency tests', () => { serverStreams: serverStreams1, socket: socket1, config: { - tlsConfig: certFixtures.tlsConfigMemRSA1, + key: tlsConfig1.key, + cert: tlsConfig1.cert, verifyPeer: false, logKeys: './tmp/key1.log', initialMaxStreamsBidi: 10000, @@ -482,7 +502,8 @@ describe('Concurrency tests', () => { serverStreams: serverStreams2, socket: socket2, config: { - tlsConfig: certFixtures.tlsConfigMemRSA2, + key: tlsConfig2.key, + cert: tlsConfig2.cert, verifyPeer: false, logKeys: './tmp/key2.log', initialMaxStreamsBidi: 10000, @@ -502,7 +523,9 @@ describe('Concurrency tests', () => { host: '127.0.0.1' as Host, port: socket2.port, socket: socket1, - crypto, + crypto: { + ops: clientCrypto, + }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, @@ -523,7 +546,9 @@ describe('Concurrency tests', () => { host: '127.0.0.1' as Host, port: socket1.port, socket: socket2, - crypto, + crypto: { + ops: clientCrypto, + }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, @@ -573,8 +598,8 @@ describe('Concurrency tests', () => { ); } logger.info('CLOSING SOCKETS'); - await socket1.stop(true); - await socket2.stop(true); + await socket1.stop({ force: true }); + await socket2.stop({ force: true }); logger.info('TEST FULLY DONE!'); }, { numRuns: 1 }, From 798014ba379d7a224f01b8b6335487accedb7f71 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Jun 2023 13:00:30 +1000 Subject: [PATCH 18/22] lint: fixing up linting errors [ci skip] --- tests/QUICServer.test.ts | 19 +- tests/QUICStream.test.ts | 1 - .../quiche.connection.lifecycle.test.ts | 92 +++--- tests/native/quiche.stream.lifecycle.test.ts | 11 +- tests/native/quiche.test.ts | 3 +- tests/native/quiche.tls.test.ts | 306 +++++++++--------- tests/tlsUtils.ts | 214 ------------ 7 files changed, 212 insertions(+), 434 deletions(-) delete mode 100644 tests/tlsUtils.ts diff --git a/tests/QUICServer.test.ts b/tests/QUICServer.test.ts index 726cf5e2..16295f22 100644 --- a/tests/QUICServer.test.ts +++ b/tests/QUICServer.test.ts @@ -1,20 +1,7 @@ import type { X509Certificate } from '@peculiar/x509'; -import type { - QUICConfig, - Host, - Hostname, - Port, - ClientCrypto, - ServerCrypto, -} from '@/types'; -import dgram from 'dgram'; +import type { Host, Hostname, ServerCrypto } from '@/types'; import Logger, { LogLevel, StreamHandler, formatting } from '@matrixai/logger'; import QUICServer from '@/QUICServer'; -import QUICConnectionId from '@/QUICConnectionId'; -import QUICConnection from '@/QUICConnection'; -import QUICSocket from '@/QUICSocket'; -import { clientDefault, buildQuicheConfig } from '@/config'; -import { quiche } from '@/native'; import * as utils from '@/utils'; import * as testsUtils from './utils'; @@ -84,14 +71,10 @@ describe(QUICServer.name, () => { certEd25519PEM = testsUtils.certToPEM(certEd25519); }); // This has to be setup asynchronously due to key generation - let clientCrypto: ClientCrypto; let serverCrypto: ServerCrypto; let key: ArrayBuffer; beforeEach(async () => { key = await testsUtils.generateKeyHMAC(); - clientCrypto = { - randomBytes: testsUtils.randomBytes, - }; serverCrypto = { sign: testsUtils.signHMAC, verify: testsUtils.verifyHMAC, diff --git a/tests/QUICStream.test.ts b/tests/QUICStream.test.ts index e38bd44f..14b95d2f 100644 --- a/tests/QUICStream.test.ts +++ b/tests/QUICStream.test.ts @@ -8,7 +8,6 @@ import * as utils from '@/utils'; import QUICServer from '@/QUICServer'; import QUICClient from '@/QUICClient'; import QUICStream from '@/QUICStream'; -import { tlsConfigWithCaArb, tlsConfigWithCaGENOKPArb } from './tlsUtils'; import * as testsUtils from './utils'; describe(QUICStream.name, () => { diff --git a/tests/native/quiche.connection.lifecycle.test.ts b/tests/native/quiche.connection.lifecycle.test.ts index 1d540b0f..df1da66a 100644 --- a/tests/native/quiche.connection.lifecycle.test.ts +++ b/tests/native/quiche.connection.lifecycle.test.ts @@ -1,5 +1,11 @@ import type { X509Certificate } from '@peculiar/x509'; -import type { QUICConfig, Crypto, Host, Hostname, Port } from '@/types'; +import type { + QUICConfig, + Host, + Port, + ClientCrypto, + ServerCrypto, +} from '@/types'; import type { Config, Connection, SendInfo } from '@/native/types'; import { quiche } from '@/native'; import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; @@ -10,7 +16,7 @@ import * as testsUtils from '../utils'; describe('quiche connection lifecycle', () => { let crypto: { key: ArrayBuffer; - ops: Crypto; + ops: ClientCrypto & ServerCrypto; }; let keyPairRSA: { publicKey: JsonWebKey; @@ -181,10 +187,10 @@ describe('quiche connection lifecycle', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let _clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; - let serverQuicheConfig: Config; + let _serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientConn: Connection; beforeAll(async () => { @@ -200,7 +206,7 @@ describe('quiche connection lifecycle', () => { maxIdleTimeout: 2000, }; clientQuicheConfig = buildQuicheConfig(clientConfig); - serverQuicheConfig = buildQuicheConfig(serverConfig); + _serverQuicheConfig = buildQuicheConfig(serverConfig); }); test('client connect', async () => { // Randomly genrate the client SCID @@ -216,7 +222,7 @@ describe('quiche connection lifecycle', () => { ); }); test('client dialing timeout', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [_clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); expect(() => clientConn.send(clientBuffer)).toThrow('Done'); // Exahust the timeout await testsUtils.waitForTimeoutNull(clientConn); @@ -245,16 +251,16 @@ describe('quiche connection lifecycle', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let _serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -286,7 +292,7 @@ describe('quiche connection lifecycle', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -318,7 +324,7 @@ describe('quiche connection lifecycle', () => { to: clientHost, from: serverHost, }); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -339,7 +345,7 @@ describe('quiche connection lifecycle', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; expect(serverConn.timeout()).toBeNull(); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, @@ -351,7 +357,7 @@ describe('quiche connection lifecycle', () => { }); test('client <-initial- server timeout', async () => { // Server tries sending the initial frame - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [_serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); expect(clientConn.timeout()).not.toBeNull(); expect(serverConn.timeout()).not.toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -390,16 +396,16 @@ describe('quiche connection lifecycle', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -431,7 +437,7 @@ describe('quiche connection lifecycle', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -463,7 +469,7 @@ describe('quiche connection lifecycle', () => { to: clientHost, from: serverHost, }); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -484,7 +490,7 @@ describe('quiche connection lifecycle', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; expect(serverConn.timeout()).toBeNull(); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, @@ -495,21 +501,21 @@ describe('quiche connection lifecycle', () => { expect(serverConn.timeout()).not.toBeNull(); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server timeout', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); expect(clientConn.timeout()).not.toBeNull(); expect(serverConn.timeout()).not.toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -548,16 +554,16 @@ describe('quiche connection lifecycle', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -589,7 +595,7 @@ describe('quiche connection lifecycle', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -621,7 +627,7 @@ describe('quiche connection lifecycle', () => { to: clientHost, from: serverHost, }); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -642,7 +648,7 @@ describe('quiche connection lifecycle', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; expect(serverConn.timeout()).toBeNull(); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, @@ -653,21 +659,21 @@ describe('quiche connection lifecycle', () => { expect(serverConn.timeout()).not.toBeNull(); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -677,7 +683,7 @@ describe('quiche connection lifecycle', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> sever', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -687,7 +693,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server timeout', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); expect(clientConn.timeout()).not.toBeNull(); expect(serverConn.timeout()).not.toBeNull(); expect(clientConn.isTimedOut()).toBeFalse(); @@ -729,7 +735,7 @@ describe('quiche connection lifecycle', () => { // These buffers will be used between the tests and will be mutated let clientSendLength: number, clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; @@ -967,7 +973,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isDraining()).toBeFalse(); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Server's responds with an initial frame expect(serverSendLength).toBe(1200); // The server is now setting its timeout to start at 1 second @@ -1021,7 +1027,7 @@ describe('quiche connection lifecycle', () => { }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -1086,7 +1092,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -1257,7 +1263,7 @@ describe('quiche connection lifecycle', () => { // These buffers will be used between the tests and will be mutated let clientSendLength: number, clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; @@ -1478,7 +1484,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isDraining()).toBeFalse(); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Server's responds with an initial frame expect(serverSendLength).toBe(1200); // The server is now setting its timeout to start at 1 second @@ -1538,7 +1544,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -1698,7 +1704,7 @@ describe('quiche connection lifecycle', () => { // These buffers will be used between the tests and will be mutated let clientSendLength: number, clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; @@ -1919,7 +1925,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isDraining()).toBeFalse(); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Server's responds with an initial frame expect(serverSendLength).toBe(1200); // The server is now setting its timeout to start at 1 second @@ -1979,7 +1985,7 @@ describe('quiche connection lifecycle', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, diff --git a/tests/native/quiche.stream.lifecycle.test.ts b/tests/native/quiche.stream.lifecycle.test.ts index 43d9f7fa..2aa38fd3 100644 --- a/tests/native/quiche.stream.lifecycle.test.ts +++ b/tests/native/quiche.stream.lifecycle.test.ts @@ -1,8 +1,7 @@ import type { Connection, StreamIter } from '@/native'; import type { ClientCrypto, Host, Port, ServerCrypto } from '@'; -import { Host as HostPort, quiche } from '@/native'; +import { quiche } from '@/native'; import QUICConnectionId from '@/QUICConnectionId'; -import { QUICConfig } from '@'; import { buildQuicheConfig, clientDefault, serverDefault } from '@/config'; import * as utils from '@/utils'; import * as testsUtils from '../utils'; @@ -31,10 +30,10 @@ function iterToArray(iter: StreamIter) { * Does all the steps for initiating a stream on both sides. * Used as a starting point for a bunch of tests. */ -function initStreamState( +function _initStreamState( connectionSource: Connection, connectionDestination: Connection, - streamId: number, + _streamId: number, ) { const message = Buffer.from('Message'); connectionSource.streamSend(0, message, false); @@ -100,7 +99,7 @@ describe('quiche stream lifecycle', () => { ); const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + const [clientSendLength] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -133,7 +132,7 @@ describe('quiche stream lifecycle', () => { }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength] = clientConn.send(clientBuffer); // Server accept serverConn = quiche.Connection.accept( diff --git a/tests/native/quiche.test.ts b/tests/native/quiche.test.ts index 0f0f0ac0..c8ada965 100644 --- a/tests/native/quiche.test.ts +++ b/tests/native/quiche.test.ts @@ -3,8 +3,7 @@ import * as testsUtils from '../utils'; describe('quiche', () => { test('frame parsing', async () => { - let frame: Buffer; - frame = Buffer.from('hello world'); + const frame = Buffer.from('hello world'); expect(() => quiche.Header.fromSlice(frame, quiche.MAX_CONN_ID_LEN), ).toThrow('BufferTooShort'); diff --git a/tests/native/quiche.tls.test.ts b/tests/native/quiche.tls.test.ts index 7d2e345b..92d1a7f0 100644 --- a/tests/native/quiche.tls.test.ts +++ b/tests/native/quiche.tls.test.ts @@ -1,5 +1,11 @@ import type { X509Certificate } from '@peculiar/x509'; -import type { QUICConfig, Crypto, Host, Hostname, Port } from '@/types'; +import type { + QUICConfig, + Host, + Port, + ClientCrypto, + ServerCrypto, +} from '@/types'; import type { Config, Connection, SendInfo } from '@/native/types'; import { quiche } from '@/native'; import { clientDefault, serverDefault, buildQuicheConfig } from '@/config'; @@ -11,7 +17,7 @@ import * as testsUtils from '../utils'; describe('quiche tls', () => { let crypto: { key: ArrayBuffer; - ops: Crypto; + ops: ClientCrypto & ServerCrypto; }; let keyPairRSA: { publicKey: JsonWebKey; @@ -91,16 +97,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -135,7 +141,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -165,7 +171,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -188,28 +194,28 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -219,7 +225,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -229,7 +235,7 @@ describe('quiche tls', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -241,7 +247,7 @@ describe('quiche tls', () => { }); }); test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -265,7 +271,7 @@ describe('quiche tls', () => { }); test('client close', async () => { clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); await testsUtils.sleep(clientConn.timeout()!); clientConn.onTimeout(); await testsUtils.waitForTimeoutNull(clientConn); @@ -293,16 +299,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -336,7 +342,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -366,7 +372,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -389,28 +395,28 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -420,7 +426,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); // Server rejects client handshake expect(() => serverConn.recv(clientBuffer.subarray(0, clientSendLength), { @@ -444,7 +450,7 @@ describe('quiche tls', () => { expect(serverConn.isDraining()).toBeFalse(); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -495,16 +501,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -538,7 +544,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -568,7 +574,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -591,28 +597,28 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server handshake expect(() => clientConn.recv(serverBuffer.subarray(0, serverSendLength), { @@ -638,7 +644,7 @@ describe('quiche tls', () => { expect(clientConn.isDraining()).toBeFalse(); }); test('client -handshake-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderHandshake = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -696,16 +702,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -740,7 +746,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -770,7 +776,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -793,28 +799,28 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -824,7 +830,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -835,7 +841,7 @@ describe('quiche tls', () => { }); test('server close early', async () => { serverConn.close(false, 304, Buffer.from('Custom TLS failed')); - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); expect(serverConn.localError()).toEqual({ isApp: false, @@ -902,16 +908,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -946,7 +952,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -976,7 +982,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -999,28 +1005,28 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, }); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -1030,7 +1036,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -handshake-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -1041,7 +1047,7 @@ describe('quiche tls', () => { }); test('client close early', async () => { clientConn.close(false, 304, Buffer.from('Custom TLS failed')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); expect(clientConn.localError()).toEqual({ isApp: false, @@ -1108,16 +1114,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -1152,7 +1158,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -1182,7 +1188,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1205,14 +1211,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -1222,7 +1228,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -1232,7 +1238,7 @@ describe('quiche tls', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -1244,7 +1250,7 @@ describe('quiche tls', () => { }); }); test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1268,7 +1274,7 @@ describe('quiche tls', () => { }); test('client close', async () => { clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); await testsUtils.sleep(clientConn.timeout()!); clientConn.onTimeout(); await testsUtils.waitForTimeoutNull(clientConn); @@ -1296,16 +1302,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -1339,7 +1345,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -1369,7 +1375,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1392,14 +1398,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -1409,7 +1415,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); expect(() => serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, @@ -1432,7 +1438,7 @@ describe('quiche tls', () => { expect(serverConn.isDraining()).toBeFalse(); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -1490,16 +1496,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -1533,7 +1539,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -1563,7 +1569,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1586,14 +1592,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server initial expect(() => clientConn.recv(serverBuffer.subarray(0, serverSendLength), { @@ -1617,7 +1623,7 @@ describe('quiche tls', () => { expect(clientConn.isDraining()).toBeFalse(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1675,16 +1681,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -1719,7 +1725,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -1749,7 +1755,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1772,14 +1778,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -1789,7 +1795,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -1801,7 +1807,7 @@ describe('quiche tls', () => { test('server close early', async () => { serverConn.close(false, 304, Buffer.from('Custom TLS failed')); - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); expect(serverConn.localError()).toEqual({ isApp: false, @@ -1868,16 +1874,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -1912,7 +1918,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -1942,7 +1948,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -1965,14 +1971,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -1982,7 +1988,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -1994,7 +2000,7 @@ describe('quiche tls', () => { test('client close early', async () => { clientConn.close(false, 304, Buffer.from('Custom TLS failed')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); expect(clientConn.localError()).toEqual({ isApp: false, @@ -2061,16 +2067,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -2105,7 +2111,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -2135,7 +2141,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2158,14 +2164,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -2175,7 +2181,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, @@ -2185,7 +2191,7 @@ describe('quiche tls', () => { expect(serverConn.isEstablished()).toBeTrue(); }); test('client <-short- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderShort = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -2197,7 +2203,7 @@ describe('quiche tls', () => { }); }); test('client -short-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderShort = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2221,7 +2227,7 @@ describe('quiche tls', () => { }); test('client close', async () => { clientConn.close(true, 0, Buffer.from('')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); await testsUtils.sleep(clientConn.timeout()!); clientConn.onTimeout(); await testsUtils.waitForTimeoutNull(clientConn); @@ -2249,16 +2255,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -2292,7 +2298,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -2322,7 +2328,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2345,14 +2351,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); clientConn.recv(serverBuffer.subarray(0, serverSendLength), { to: clientHost, from: serverHost, @@ -2362,7 +2368,7 @@ describe('quiche tls', () => { expect(clientConn.isEstablished()).toBeTrue(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); expect(() => serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, @@ -2385,7 +2391,7 @@ describe('quiche tls', () => { expect(serverConn.isDraining()).toBeFalse(); }); test('client <-handshake- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); const serverHeaderHandshake = quiche.Header.fromSlice( serverBuffer.subarray(0, serverSendLength), quiche.MAX_CONN_ID_LEN, @@ -2443,16 +2449,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -2486,7 +2492,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -2516,7 +2522,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2539,14 +2545,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server initial expect(() => clientConn.recv(serverBuffer.subarray(0, serverSendLength), { @@ -2572,7 +2578,7 @@ describe('quiche tls', () => { expect(clientConn.isDraining()).toBeFalse(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2632,16 +2638,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -2676,7 +2682,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -2706,7 +2712,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2729,14 +2735,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server initial expect(() => clientConn.recv(serverBuffer.subarray(0, serverSendLength), { @@ -2757,7 +2763,7 @@ describe('quiche tls', () => { expect(clientConn.isDraining()).toBeFalse(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2791,7 +2797,7 @@ describe('quiche tls', () => { }); test('server close early', async () => { serverConn.close(false, 304, Buffer.from('Custom TLS failed')); - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); expect(serverConn.localError()).toEqual({ isApp: false, @@ -2858,16 +2864,16 @@ describe('quiche tls', () => { port: 55556, }; // These buffers will be used between the tests and will be mutated - let clientSendLength: number, clientSendInfo: SendInfo; + let clientSendLength: number, _clientSendInfo: SendInfo; const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - let serverSendLength: number, serverSendInfo: SendInfo; + let serverSendLength: number, _serverSendInfo: SendInfo; const serverBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); let clientQuicheConfig: Config; let serverQuicheConfig: Config; let clientScid: QUICConnectionId; let clientDcid: QUICConnectionId; let serverScid: QUICConnectionId; - let serverDcid: QUICConnectionId; + let _serverDcid: QUICConnectionId; let clientConn: Connection; let serverConn: Connection; beforeAll(async () => { @@ -2902,7 +2908,7 @@ describe('quiche tls', () => { ); }); test('client dialing', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); }); test('client and server negotiation', async () => { const clientHeaderInitial = quiche.Header.fromSlice( @@ -2932,7 +2938,7 @@ describe('quiche tls', () => { from: serverHost, }); // Client will retry the initial packet with the token - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitialRetry = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -2955,14 +2961,14 @@ describe('quiche tls', () => { serverQuicheConfig, ); clientDcid = serverScid; - serverDcid = clientScid; + _serverDcid = clientScid; serverConn.recv(clientBuffer.subarray(0, clientSendLength), { to: serverHost, from: clientHost, }); }); test('client <-initial- server', async () => { - [serverSendLength, serverSendInfo] = serverConn.send(serverBuffer); + [serverSendLength, _serverSendInfo] = serverConn.send(serverBuffer); // Client rejects server initial expect(() => clientConn.recv(serverBuffer.subarray(0, serverSendLength), { @@ -2983,7 +2989,7 @@ describe('quiche tls', () => { expect(clientConn.isDraining()).toBeFalse(); }); test('client -initial-> server', async () => { - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -3017,7 +3023,7 @@ describe('quiche tls', () => { }); test('client close early', async () => { clientConn.close(false, 304, Buffer.from('Custom TLS failed')); - [clientSendLength, clientSendInfo] = clientConn.send(clientBuffer); + [clientSendLength, _clientSendInfo] = clientConn.send(clientBuffer); expect(clientConn.localError()).toEqual({ isApp: false, diff --git a/tests/tlsUtils.ts b/tests/tlsUtils.ts deleted file mode 100644 index adab0bbb..00000000 --- a/tests/tlsUtils.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { X509Certificate } from '@peculiar/x509'; -import * as x509 from '@peculiar/x509'; -import * as asn1 from '@peculiar/asn1-schema'; -import * as asn1X509 from '@peculiar/asn1-x509'; -import * as asn1Pkcs8 from '@peculiar/asn1-pkcs8'; -import { fc } from '@fast-check/jest'; -import { Crypto } from '@peculiar/webcrypto'; -import sodium from 'sodium-native'; -import * as testsUtils from './utils'; - -/** - * WebCrypto polyfill from @peculiar/webcrypto - * This behaves differently with respect to Ed25519 keys - * See: https://github.com/PeculiarVentures/webcrypto/issues/55 - */ -const webcrypto = new Crypto(); - -/** - * Monkey patches the global crypto object polyfill - */ -globalThis.crypto = webcrypto; - -// Setting provider -x509.cryptoProvider.set(webcrypto); - -/** - * Imports Ed25519 public `CryptoKey` from key buffer. - * If `publicKey` is already `CryptoKey`, then this just returns it. - */ -async function importPublicKey(publicKey: BufferSource): Promise { - return webcrypto.subtle.importKey( - 'raw', - publicKey, - { - name: 'EdDSA', - namedCurve: 'Ed25519', - }, - true, - ['verify'], - ); -} - -/** - * Imports Ed25519 private `CryptoKey` from key buffer. - * If `privateKey` is already `CryptoKey`, then this just returns it. - */ -async function importPrivateKey(privateKey: BufferSource): Promise { - return await webcrypto.subtle.importKey( - 'jwk', - { - alg: 'EdDSA', - kty: 'OKP', - crv: 'Ed25519', - d: bufferWrap(privateKey).toString('base64url'), - }, - { - name: 'EdDSA', - namedCurve: 'Ed25519', - }, - true, - ['sign'], - ); -} - -/** - * Zero-copy wraps ArrayBuffer-like objects into Buffer - * This supports ArrayBuffer, TypedArrays and the NodeJS Buffer - */ -function bufferWrap( - array: BufferSource, - offset?: number, - length?: number, -): Buffer { - if (Buffer.isBuffer(array)) { - return array; - } else if (ArrayBuffer.isView(array)) { - return Buffer.from( - array.buffer, - offset ?? array.byteOffset, - length ?? array.byteLength, - ); - } else { - return Buffer.from(array, offset, length); - } -} - -function privateKeyToPEM(privateKey: Buffer): string { - const pkcs8 = new asn1Pkcs8.PrivateKeyInfo({ - privateKeyAlgorithm: new asn1X509.AlgorithmIdentifier({ - algorithm: x509.idEd25519, - }), - privateKey: new asn1Pkcs8.PrivateKey( - new asn1.OctetString(privateKey).toASN().toBER(), - ), - }); - const data = Buffer.from(asn1.AsnSerializer.serialize(pkcs8)); - const contents = - data - .toString('base64') - .replace(/(.{64})/g, '$1\n') - .trimEnd() + '\n'; - return `-----BEGIN PRIVATE KEY-----\n${contents}-----END PRIVATE KEY-----\n`; -} - -function certToPEM(cert: X509Certificate): string { - return cert.toString('pem') + '\n'; -} - -type KeyPair = { - privateKey: Buffer; - publicKey: Buffer; -}; - -/** - * Extracts Ed25519 Public Key from Ed25519 Private Key - * The returned buffers are guaranteed to unpooled. - * This means the underlying `ArrayBuffer` is safely transferrable. - */ -function publicKeyFromPrivateKeyEd25519(privateKey: Buffer): Buffer { - const publicKey = Buffer.allocUnsafeSlow(sodium.crypto_sign_PUBLICKEYBYTES); - sodium.crypto_sign_seed_keypair( - publicKey, - Buffer.allocUnsafe(sodium.crypto_sign_SECRETKEYBYTES), - privateKey, - ); - return publicKey; -} - -const privateKeyArb = fc - .uint8Array({ - minLength: 32, - maxLength: 32, - }) - .map((v) => Buffer.from(v)); - -const publicKeyArb = (privateKey: fc.Arbitrary = privateKeyArb) => - privateKey.map((privateKey) => publicKeyFromPrivateKeyEd25519(privateKey)); - -const keyPairArb = ( - privateKey: fc.Arbitrary = privateKeyArb, -): fc.Arbitrary => - privateKey.chain((privateKey) => - fc.record({ - privateKey: fc.constant(privateKey), - publicKey: publicKeyArb(fc.constant(privateKey)), - }), - ); - -const keyPairsArb = (min: number = 1, max?: number) => - fc.array(keyPairArb(), { - minLength: min, - maxLength: max ?? min, - size: 'xsmall', - }); - -// Const tlsConfigArb = (keyPairs: fc.Arbitrary> = keyPairsArb()) => -// keyPairs -// .map(async (keyPairs) => await createTLSConfigWithChain(keyPairs)) -// .noShrink(); - -// const tlsConfigWithCaRSAArb = fc.record({ -// type: fc.constant('RSA'), -// ca: fc.constant(certFixtures.tlsConfigMemRSACa), -// tlsConfig: certFixtures.tlsConfigRSAExampleArb, -// }); -// -// const tlsConfigWithCaOKPArb = fc.record({ -// type: fc.constant('OKP'), -// ca: fc.constant(certFixtures.tlsConfigMemOKPCa), -// tlsConfig: certFixtures.tlsConfigOKPExampleArb, -// }); -// -// const tlsConfigWithCaECDSAArb = fc.record({ -// type: fc.constant('ECDSA'), -// ca: fc.constant(certFixtures.tlsConfigMemECDSACa), -// tlsConfig: certFixtures.tlsConfigECDSAExampleArb, -// }); - -// const tlsConfigWithCaGENOKPArb = tlsConfigArb().map(async (configProm) => { -// const config = await configProm; -// return { -// type: fc.constant('GEN-OKP'), -// tlsConfig: { -// certChainPem: config.certChainPem, -// privKeyPem: config.privKeyPem, -// }, -// ca: { -// certChainPem: config.caPem, -// privKeyPem: '', -// }, -// }; -// }); -// -// const tlsConfigWithCaArb = fc -// .oneof( -// tlsConfigWithCaRSAArb, -// tlsConfigWithCaOKPArb, -// tlsConfigWithCaECDSAArb, -// tlsConfigWithCaGENOKPArb, -// ) -// .noShrink(); - -export { - privateKeyArb, - publicKeyArb, - keyPairArb, - keyPairsArb, - // TlsConfigArb, - // tlsConfigWithCaArb, - // tlsConfigWithCaRSAArb, - // tlsConfigWithCaOKPArb, - // tlsConfigWithCaECDSAArb, - // tlsConfigWithCaGENOKPArb, -}; From 30daec8baaaab01c7256f0951f96824476b732a2 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 29 Jun 2023 17:40:14 +1000 Subject: [PATCH 19/22] tests: expanding native stream tests [ci skip] --- src/native/types.ts | 6 +- src/utils.ts | 42 + tests/native/quiche.stream.lifecycle.test.ts | 2519 +++++++++++++++++- 3 files changed, 2542 insertions(+), 25 deletions(-) diff --git a/src/native/types.ts b/src/native/types.ts index 81536b5f..1d2d920c 100644 --- a/src/native/types.ts +++ b/src/native/types.ts @@ -313,11 +313,9 @@ type PathStatsIter = { [Symbol.iterator](): Iterator; }; +export { CongestionControlAlgorithm, Shutdown, Type, ConnectionErrorCode }; + export type { - CongestionControlAlgorithm, - Shutdown, - Type, - ConnectionErrorCode, ConnectionError, Stats, HostPort as Host, diff --git a/src/utils.ts b/src/utils.ts index 979f3e0d..283dc652 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,7 @@ import type { Hostname, ServerCrypto, } from './types'; +import type { Connection } from '@/native'; import dns from 'dns'; import { IPv4, IPv6, Validator } from 'ip-num'; import QUICConnectionId from './QUICConnectionId'; @@ -387,6 +388,46 @@ async function sleep(ms: number): Promise { return await new Promise((r) => setTimeout(r, ms)); } +/** + * Useful for debug printing stream state + */ +function streamStats( + connection: Connection, + streamId: number, + label: string, +): string { + let streamWritable: string; + try { + streamWritable = `${connection.streamWritable(streamId, 0)}`; + } catch (e) { + streamWritable = `threw ${e.message}`; + } + let streamCapacity: string; + try { + streamCapacity = `${connection.streamCapacity(streamId)}`; + } catch (e) { + streamCapacity = `threw ${e.message}`; + } + let readableIterator = false; + for (const streamIterElement of connection.readable()) { + if (streamIterElement === streamId) readableIterator = true; + } + let writableIterator = false; + for (const streamIterElement of connection.writable()) { + if (streamIterElement === streamId) writableIterator = true; + } + return ` + ---${label}--- + isReadable: ${connection.isReadable()}, + readable iterator: ${readableIterator}, + streamReadable: ${connection.streamReadable(streamId)}, + streamFinished: ${connection.streamFinished(streamId)}, + writable iterator: ${writableIterator}, + streamWritable: ${streamWritable}, + streamCapacity: ${streamCapacity}, +`; +} + export { isIPv4, isIPv6, @@ -413,4 +454,5 @@ export { mintToken, validateToken, sleep, + streamStats, }; diff --git a/tests/native/quiche.stream.lifecycle.test.ts b/tests/native/quiche.stream.lifecycle.test.ts index 2aa38fd3..a1c59d09 100644 --- a/tests/native/quiche.stream.lifecycle.test.ts +++ b/tests/native/quiche.stream.lifecycle.test.ts @@ -1,9 +1,10 @@ import type { Connection, StreamIter } from '@/native'; import type { ClientCrypto, Host, Port, ServerCrypto } from '@'; -import { quiche } from '@/native'; +import { quiche, Shutdown } from '@/native'; import QUICConnectionId from '@/QUICConnectionId'; import { buildQuicheConfig, clientDefault, serverDefault } from '@/config'; import * as utils from '@/utils'; +import { sleep } from '@/utils'; import * as testsUtils from '../utils'; function sendPacket( @@ -30,16 +31,18 @@ function iterToArray(iter: StreamIter) { * Does all the steps for initiating a stream on both sides. * Used as a starting point for a bunch of tests. */ -function _initStreamState( +function setupStreamState( connectionSource: Connection, connectionDestination: Connection, - _streamId: number, + streamId: number, ) { const message = Buffer.from('Message'); - connectionSource.streamSend(0, message, false); + connectionSource.streamSend(streamId, message, false); sendPacket(connectionSource, connectionDestination); - - throw Error('TMP IMP'); + sendPacket(connectionDestination, connectionSource); + // Clearing message buffer + const buffer = Buffer.allocUnsafe(1024); + connectionDestination.streamRecv(streamId, buffer); } describe('quiche stream lifecycle', () => { @@ -84,9 +87,10 @@ describe('quiche stream lifecycle', () => { key: tlsConfigServer.key, cert: tlsConfigServer.cert, + logKeys: './tmp/key.log', }); - // Randomly genrate the client SCID + // Randomly generate the client SCID const scidBuffer = new ArrayBuffer(quiche.MAX_CONN_ID_LEN); await crypto.ops.randomBytes(scidBuffer); const clientScid = new QUICConnectionId(scidBuffer); @@ -99,7 +103,7 @@ describe('quiche stream lifecycle', () => { ); const clientBuffer = Buffer.allocUnsafe(quiche.MAX_DATAGRAM_SIZE); - const [clientSendLength] = clientConn.send(clientBuffer); + let [clientSendLength] = clientConn.send(clientBuffer); const clientHeaderInitial = quiche.Header.fromSlice( clientBuffer.subarray(0, clientSendLength), quiche.MAX_CONN_ID_LEN, @@ -315,21 +319,31 @@ describe('quiche stream lifecycle', () => { expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); }); - test('closing forward stream with fin frame', async () => { + }); + describe('stream finishes with 0-len fin', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('closing forward stream with 0-len fin frame', async () => { clientConn.streamSend(0, new Uint8Array(0), true); expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not in the writable iterator expect(iterToArray(clientConn.writable())).not.toContain(0); expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.streamFinished(0)).toBeFalse(); expect(clientConn.streamReadable(0)).toBeFalse(); + // But still technically writable expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); expect(clientConn.streamWritable(0, 0)).toBeTrue(); sendPacket(clientConn, serverConn); - // Client state + // Client state, no changes expect(iterToArray(clientConn.readable())).not.toContain(0); - // Not writable anymore expect(iterToArray(clientConn.writable())).not.toContain(0); expect(clientConn.isReadable()).toBeFalse(); expect(clientConn.streamFinished(0)).toBeFalse(); @@ -337,19 +351,25 @@ describe('quiche stream lifecycle', () => { expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); expect(clientConn.streamWritable(0, 0)).toBeTrue(); + // Further writes throws `FinalSize` + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Server state expect(iterToArray(serverConn.readable())).toContain(0); expect(iterToArray(serverConn.writable())).toContain(0); expect(serverConn.isReadable()).toBeTrue(); - // Is finished + // Stream is immediately finished due to no buffered data expect(serverConn.streamFinished(0)).toBeTrue(); - // Still readable + // Still readable due to 0-len message and fin flag expect(serverConn.streamReadable(0)).toBeTrue(); expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); expect(serverConn.streamWritable(0, 0)).toBeTrue(); // Reading message const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + // Message is empty but exists due to fin flag expect(bytes).toEqual(0); expect(fin).toBe(true); @@ -357,6 +377,7 @@ describe('quiche stream lifecycle', () => { // Nothing left to read expect(serverConn.isReadable()).toBeFalse(); expect(serverConn.streamReadable(0)).toBeFalse(); + // Further reads throw `Done` expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); // Server sends ack back @@ -384,7 +405,7 @@ describe('quiche stream lifecycle', () => { expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); }); - test('closing reverse stream with fin frame', async () => { + test('closing reverse stream with 0-len fin frame', async () => { serverConn.streamSend(0, new Uint8Array(0), true); expect(iterToArray(serverConn.readable())).not.toContain(0); expect(iterToArray(serverConn.writable())).not.toContain(0); @@ -430,10 +451,10 @@ describe('quiche stream lifecycle', () => { expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( 'InvalidStreamState(0)', ); - // Server sends ack back sendPacket(clientConn, serverConn); - + }); + test('server state is cleaned up and invalid', async () => { // Server state expect(iterToArray(serverConn.readable())).not.toContain(0); expect(iterToArray(serverConn.writable())).not.toContain(0); @@ -446,7 +467,184 @@ describe('quiche stream lifecycle', () => { expect(() => serverConn.streamWritable(0, 0)).toThrow( 'InvalidStreamState(0)', ); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('Done'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream finishes with data fin', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('closing forward stream with data fin frame', async () => { + clientConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream is not finished due to buffered data + expect(serverConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered data + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + // Message is empty but exists due to fin flag + expect(bytes).toEqual(7); + expect(fin).toBe(true); + expect(streamBuf.subarray(0, bytes).toString()).toEqual('message'); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('closing reverse stream with data fin frame', async () => { + serverConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Stream is not finished due to buffered data + expect(clientConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered data + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(7); + expect(fin).toBe(true); + expect(streamBuf.subarray(0, bytes).toString()).toEqual('message'); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { // Client state expect(iterToArray(clientConn.readable())).not.toContain(0); expect(iterToArray(clientConn.writable())).not.toContain(0); @@ -459,15 +657,2294 @@ describe('quiche stream lifecycle', () => { expect(() => clientConn.streamWritable(0, 0)).toThrow( 'InvalidStreamState(0)', ); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream finishes with buffered data and data fin', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('sending multiple messages on forward stream', async () => { + clientConn.streamSend(0, Buffer.from('Message1 '), false); + clientConn.streamSend(0, Buffer.from('Message2 '), false); + clientConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + }); + test('send multiple messages with a fin frame on forward stream', async () => { + clientConn.streamSend(0, Buffer.from('Message1 '), false); + clientConn.streamSend(0, Buffer.from('Message2 '), false); + clientConn.streamSend(0, Buffer.from('Message3 '), true); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + expect(bytes).toBe(54); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 Message1 Message2 Message3 ', + ); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + }); + test('extra writes and reads are invalid on forward stream', async () => { + expect(() => + clientConn.streamSend(0, Buffer.from('invalid1'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('invalid2'), true), + ).toThrow('FinalSize'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + }); + test('sending multiple messages on reverse stream', async () => { + serverConn.streamSend(0, Buffer.from('Message1 '), false); + serverConn.streamSend(0, Buffer.from('Message2 '), false); + serverConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + }); + test('send multiple messages with a fin frame on reverse stream', async () => { + serverConn.streamSend(0, Buffer.from('Message1 '), false); + serverConn.streamSend(0, Buffer.from('Message2 '), false); + serverConn.streamSend(0, Buffer.from('Message3 '), true); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toBe(54); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 Message1 Message2 Message3 ', + ); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('Done'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { // No new packets expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); }); }); - }); -}); + describe('stream finishes with buffered data and 0-len fin', () => { + // Notes: + // The isFinished doesn't return true until buffered data is read. + // Reading out buffered data will have fin flag be true. + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('sending multiple messages on forward stream', async () => { + clientConn.streamSend(0, Buffer.from('Message1 '), false); + clientConn.streamSend(0, Buffer.from('Message2 '), false); + clientConn.streamSend(0, Buffer.from('Message3 '), false); -// TODO: -// Stream only finishes after reading out data. -// test permutations of force closing sending/receiving, by client or server, with and without buffered data. + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + }); + test('send 0-len fin on forward stream', async () => { + clientConn.streamSend(0, new Uint8Array(0), true); + + // Only one packet is sent + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); // Ack + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + expect(serverConn.streamReadable(0)).toBeTrue(); + // Finished is still false + expect(serverConn.streamFinished(0)).toBeFalse(); + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + expect(bytes).toBe(27); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 ', + ); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + }); + test('extra writes and reads are invalid on forward stream', async () => { + expect(() => + clientConn.streamSend(0, Buffer.from('invalid1'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('invalid2'), true), + ).toThrow('FinalSize'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + }); + test('sending multiple messages on reverse stream', async () => { + serverConn.streamSend(0, Buffer.from('Message1 '), false); + serverConn.streamSend(0, Buffer.from('Message2 '), false); + serverConn.streamSend(0, Buffer.from('Message3 '), false); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + }); + test('send 0-len fin on reverse stream', async () => { + serverConn.streamSend(0, new Uint8Array(0), true); + + // Only one packet is sent + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); // Ack + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + expect(clientConn.streamReadable(0)).toBeTrue(); + // Finished is still false due to buffered data + expect(clientConn.streamFinished(0)).toBeFalse(); + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toBe(27); + expect(fin).toBeTrue(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1 Message2 Message3 ', + ); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream finishes with 0-len fin before any data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + }); + + test('initializing stream with no data', async () => { + clientConn.streamSend(0, new Uint8Array(0), false); + + // Local state exists + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + + // No packets are sent, therefor no remote state created + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + test('closing forward stream with 0-len fin frame', async () => { + clientConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Further writes fail + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream is immediately finished due to no buffered data + expect(serverConn.streamFinished(0)).toBeTrue(); + // Still readable due to 0-len message and fin flag + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + // Message is empty but exists due to fin flag + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('closing reverse stream with 0-len fin frame', async () => { + serverConn.streamSend(0, new Uint8Array(0), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Is finished + expect(clientConn.streamFinished(0)).toBeTrue(); + // Still readable + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(0); + expect(fin).toBe(true); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream finishes with data fin before any data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + }); + + test('initializing stream with no data', async () => { + clientConn.streamSend(0, new Uint8Array(0), false); + + // Local state exists + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + + // No packets are sent, therefor no remote state created + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + test('closing forward stream with data fin frame', async () => { + clientConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(clientConn, serverConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeTrue(); + // Stream not finished due to buffered data + expect(serverConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered and fin flag + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + // Message is empty but exists due to fin flag + expect(bytes).toEqual(7); + expect(fin).toBe(true); + + expect(serverConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Server sends ack back + sendPacket(serverConn, clientConn); + + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('closing reverse stream with data fin frame', async () => { + serverConn.streamSend(0, Buffer.from('message'), true); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + sendPacket(serverConn, clientConn); + + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + // Not writable anymore + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamCapacity(0)).toBeGreaterThan(0); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + + // Client state + expect(iterToArray(clientConn.readable())).toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeTrue(); + // Stream not finished due to buffered data + expect(clientConn.streamFinished(0)).toBeFalse(); + // Still readable due to buffered and fin flag + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeGreaterThan(0); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + + // Reading message + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toEqual(7); + expect(fin).toBe(true); + + expect(clientConn.streamFinished(0)).toBeTrue(); + // Nothing left to read + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream state is now invalid since both streams have fully closed + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Server sends ack back + sendPacket(clientConn, serverConn); + }); + test('server state is cleaned up and invalid', async () => { + // Server state + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client state is cleaned up and invalid', async () => { + // Client state + expect(iterToArray(clientConn.readable())).not.toContain(0); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + + // Forcing stream closed tests + describe('stream forced closed by client after initial message', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + describe('closing writable from client', () => { + test('client closes writable', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(() => + clientConn.streamShutdown(0, Shutdown.Write, 42), + ).toThrow('Done'); + + // States are unchanged + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + // No longer in writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on client', async () => { + // Can't write after shutdown + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + sendPacket(clientConn, serverConn); + // Stream is both readable and finished + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).toContain(0); + }); + test('stream is no longer readable on server', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(serverConn.isReadable()).toBeFalse(); + // Stream is still readable and finished + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + // But not in the iterator + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(serverConn, clientConn); + + // No changes to stream state on server + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Client changes + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('closing readable from client', () => { + test('client closes readable', async () => { + // Initial readable state + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + + // No state change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('Stream is still readable for client', async () => { + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).toContain(0); + + // Sending packet + sendPacket(clientConn, serverConn); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // Stream writable and capacity now throws + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // But still listed as writable + expect(iterToArray(serverConn.writable())).toContain(0); + }); + test('stream no longer writable on server', async () => { + // Writes now throw + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // No longer listed as writable + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial readable states + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(serverConn, clientConn); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client stream now finished', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client responds', async () => { + sendPacket(clientConn, serverConn); // Ack? + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('stream still readable on client', async () => { + // Reading stream will never throw, but it does finish. + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + test('server final stream state', async () => { + // Server states + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client final stream state', async () => { + // Client never reaches invalid state? + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream forced closed by server after initial message', () => { + // This test proves closing is the same from the client side and server side. + // This is expected given the symmetric nature of a quic connection. + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + describe('closing writable from server', () => { + test('server closes writable', async () => { + // Initial writable states + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).toContain(0); + + // After shutting down + serverConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(() => + serverConn.streamShutdown(0, Shutdown.Write, 42), + ).toThrow('Done'); + + // States are unchanged + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + // No longer in writable iterator + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on server', async () => { + // Can't write after shutdown + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives packet and updates state', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + sendPacket(serverConn, clientConn); + // Stream is both readable and finished + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).toContain(0); + }); + test('stream is no longer readable on client', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(clientConn.isReadable()).toBeFalse(); + // Stream is still readable and finished + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeTrue(); + // But not in the iterator + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('server receives response packet and updates state', async () => { + // Initial writable states + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(clientConn, serverConn); + + // No changes to stream state on server + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // Client changes? + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).not.toContain(0); + expect(() => + serverConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('closing readable from server', () => { + test('server closes readable', async () => { + // Initial readable state + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // After shutting down + serverConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + + // No state change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('Stream is still readable for server', async () => { + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives packet and updates state', async () => { + // Initial state + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).toContain(0); + + // Sending packet + sendPacket(serverConn, clientConn); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + // Stream writable and capacity now throws + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // But still listed as writable + expect(iterToArray(clientConn.writable())).toContain(0); + }); + test('stream no longer writable on client', async () => { + // Writes now throw + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // No longer listed as writable + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives response packet and updates state', async () => { + // Initial readable states + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(clientConn, serverConn); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // Client changes + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // No changes to stream state on server + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(clientConn.writable())).not.toContain(0); + + // Server changes + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('server stream now finished', async () => { + // Reading still results in done + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('server responds', async () => { + sendPacket(serverConn, clientConn); // Ack? + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + // No changes to stream state on client + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(clientConn.writable())).not.toContain(0); + + // Server changes + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('stream still readable on server', async () => { + // Reading stream will never throw, but it does finish. + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + }); + test('client final stream state', async () => { + // Server states + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('Done'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(() => clientConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => clientConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('server final stream state', async () => { + // Client never reaches invalid state? + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream forced closed by client before initial message', () => { + // This tests the case where a stream is initiated on one side but no data is sent. + // So the state is not created on the receiving side before it is closed. + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + }); + + test('initializing stream with no data', async () => { + clientConn.streamSend(0, new Uint8Array(0), false); + + // Local state exists + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + + // No packets are sent, therefor no remote state created + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + describe('closing writable from client', () => { + test('client closes writable', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(() => + clientConn.streamShutdown(0, Shutdown.Write, 42), + ).toThrow('Done'); + + // States are unchanged + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + // No longer in writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on client', async () => { + // Can't write after shutdown + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives packet and creates state', async () => { + // No local state exists initially + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + + // Packet is sent + sendPacket(clientConn, serverConn); + // State is created + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + // And immediately closes + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).toContain(0); + }); + test('stream is no longer readable on server', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(serverConn.isReadable()).toBeFalse(); + // Stream is still readable and finished + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + // But not in the iterator + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(serverConn, clientConn); + + // No changes to stream state on server + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Client changes? + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('closing readable from client', () => { + test('client closes readable', async () => { + // Initial readable state + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + + // No state change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('Stream is still readable for client', async () => { + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).toContain(0); + + // Sending packet + sendPacket(clientConn, serverConn); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // Stream writable and capacity now throws + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // But still listed as writable + expect(iterToArray(serverConn.writable())).toContain(0); + }); + test('stream no longer writable on server', async () => { + // Writes now throw + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // No longer listed as writable + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial readable states + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(serverConn, clientConn); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client stream now finished', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client responds', async () => { + sendPacket(clientConn, serverConn); // Ack? + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('stream still readable on client', async () => { + // Reading stream will never throw, but it does finish. + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + test('server final stream state', async () => { + // Server states + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client final stream state', async () => { + // Client never reaches invalid state? + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('stream forced closed by client with buffered data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('buffering data both ways', async () => { + clientConn.streamSend(0, Buffer.from('Message1'), false); + clientConn.streamSend(0, Buffer.from('Message2'), false); + clientConn.streamSend(0, Buffer.from('Message3'), false); + + serverConn.streamSend(0, Buffer.from('Message1'), false); + serverConn.streamSend(0, Buffer.from('Message2'), false); + serverConn.streamSend(0, Buffer.from('Message3'), false); + + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); + + // No more packets to send + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + describe('closing writable from client', () => { + test('client closes writable', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Write, 42); + // Further shutdowns throw done + expect(() => + clientConn.streamShutdown(0, Shutdown.Write, 42), + ).toThrow('Done'); + + // States are unchanged + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + // No longer in writable iterator + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('stream is no longer writable on client', async () => { + // Can't write after shutdown + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Still seen as writable + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(serverConn.readable())).toContain(0); + + sendPacket(clientConn, serverConn); + + // Stream is both readable and finished + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).toContain(0); + }); + test('stream is no longer readable on server', async () => { + // Stream now throws `StreamReset` with code 42 + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + + // Connection is now not readable + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial writable states + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + + // Response is sent + sendPacket(serverConn, clientConn); + + // No changes to stream state on server + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(serverConn.readable())).not.toContain(0); + + // Client changes? + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(clientConn.writable())).not.toContain(0); + expect(() => + clientConn.streamSend(0, Buffer.from('hello'), false), + ).toThrow('FinalSize'); + }); + test('no further packets sent', async () => { + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('closing readable from client', () => { + test('client closes readable', async () => { + // Initial readable state + // Readable due to buffered data + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).toContain(0); + + // After shutting down + clientConn.streamShutdown(0, Shutdown.Read, 42); + // Further shutdowns throw done + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + + // Client ceases to be readable + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('Stream is still readable for client', async () => { + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('server receives packet and updates state', async () => { + // Initial state + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + expect(iterToArray(serverConn.writable())).toContain(0); + + // Sending packet + sendPacket(clientConn, serverConn); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // Stream writable and capacity now throws + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // But still listed as writable + expect(iterToArray(serverConn.writable())).toContain(0); + }); + test('stream no longer writable on server', async () => { + // Writes now throw + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('StreamStopped(42)'); + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + // No longer listed as writable + expect(iterToArray(serverConn.writable())).not.toContain(0); + }); + test('client receives response packet and updates state', async () => { + // Initial readable states + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // Response is sent + sendPacket(serverConn, clientConn); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + // Stream is now finished + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client stream now finished', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('client responds', async () => { + sendPacket(clientConn, serverConn); // Ack? + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No changes to stream state on server + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'StreamStopped(42)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'StreamStopped(42)', + ); + expect(iterToArray(serverConn.writable())).not.toContain(0); + + // Client changes + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('stream still readable on client', async () => { + // Reading stream will never throw, but it does finish. + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(iterToArray(clientConn.readable())).not.toContain(0); + }); + test('no more packets sent', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + test('server final stream state', async () => { + // Server states + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('StreamStopped(42)'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'StreamReset(42)', + ); + // States change + expect(() => + serverConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('Done'); + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeTrue(); + expect(() => serverConn.streamWritable(0, 0)).toThrow( + 'InvalidStreamState(0)', + ); + expect(() => serverConn.streamCapacity(0)).toThrow( + 'InvalidStreamState(0)', + ); + }); + test('client final stream state', async () => { + // Client never reaches invalid state? + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + expect(() => + clientConn.streamSend(0, Buffer.from('message'), true), + ).toThrow('FinalSize'); + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeTrue(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + + // Connection closing + // Note: + // It seems that stream states are not aware of connection states. + // So a closing stream does not trigger streams ending or even cleaning up. + // This also means, normal stream cleanup expectations don't happen. + // Stream will still be writable but throw. + // Stream will still be readable but never finish. + describe('connection closes with active stream, no buffered stream data', () => { + // Note: + // Seems like stream state is not cleaned up by the stream closing. + // We can still write to it and the capacity will change, so the writable is still being buffered? + // Do we need to close the stream to free up memory? + + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('client closing connection', async () => { + clientConn.close(true, 42, Buffer.from('some reason')); + + sendPacket(clientConn, serverConn); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + test('waiting for closed state', async () => { + await sleep(100); + await Promise.all([ + sleep((clientConn.timeout() ?? 0) + 1).then(() => + clientConn.onTimeout(), + ), + sleep((serverConn.timeout() ?? 0) + 1).then(() => + serverConn.onTimeout(), + ), + ]); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThan(12000); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('manually clean up client stream state', async () => { + clientConn.streamShutdown(0, Shutdown.Read, 42); + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + clientConn.streamShutdown(0, Shutdown.Write, 42); + expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + + // Can't send + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Still no change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + }); + test('manually clean up server stream state', async () => { + serverConn.streamShutdown(0, Shutdown.Read, 42); + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + serverConn.streamShutdown(0, Shutdown.Write, 42); + expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); + + // Can't send + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Still no change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + describe('connection closes with active stream, with buffered stream data', () => { + const streamBuf = Buffer.allocUnsafe(1024); + + beforeAll(async () => { + await setupConnectionsRSA(); + setupStreamState(clientConn, serverConn, 0); + }); + + test('buffering data both ways', async () => { + clientConn.streamSend(0, Buffer.from('Message1'), false); + clientConn.streamSend(0, Buffer.from('Message2'), false); + clientConn.streamSend(0, Buffer.from('Message3'), false); + + serverConn.streamSend(0, Buffer.from('Message1'), false); + serverConn.streamSend(0, Buffer.from('Message2'), false); + serverConn.streamSend(0, Buffer.from('Message3'), false); + + sendPacket(clientConn, serverConn); + sendPacket(serverConn, clientConn); + sendPacket(clientConn, serverConn); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('client closing connection', async () => { + clientConn.close(true, 42, Buffer.from('some reason')); + + sendPacket(clientConn, serverConn); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeFalse(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeFalse(); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeTrue(); + expect(clientConn.streamReadable(0)).toBeTrue(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBe(12000); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + const [bytes, fin] = clientConn.streamRecv(0, streamBuf); + expect(bytes).toBe(24); + expect(fin).toBeFalse(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1Message2Message3', + ); + + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeTrue(); + expect(serverConn.streamReadable(0)).toBeTrue(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBe(12000); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + const [bytes, fin] = serverConn.streamRecv(0, streamBuf); + expect(bytes).toBe(24); + expect(fin).toBeFalse(); + expect(streamBuf.subarray(0, bytes).toString()).toEqual( + 'Message1Message2Message3', + ); + + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThan(12000); + }); + test('waiting for closed state', async () => { + await sleep(100); + await Promise.all([ + sleep((clientConn.timeout() ?? 0) + 1).then(() => + clientConn.onTimeout(), + ), + sleep((serverConn.timeout() ?? 0) + 1).then(() => + serverConn.onTimeout(), + ), + ]); + expect(clientConn.timeout()).toBeNull(); + expect(serverConn.timeout()).toBeNull(); + + expect(clientConn.isDraining()).toBeTrue(); + expect(clientConn.isClosed()).toBeTrue(); + expect(serverConn.isDraining()).toBeTrue(); + expect(serverConn.isClosed()).toBeTrue(); + + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + }); + test('client stream still functions', async () => { + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + + // Can still send + expect(clientConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + }); + test('server stream still functions', async () => { + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThan(12000); + + // Can still send + expect(serverConn.streamSend(0, Buffer.from('message'), false)).toBe(7); + // Can still recv + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + test('manually clean up client stream state', async () => { + clientConn.streamShutdown(0, Shutdown.Read, 42); + expect(() => clientConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + clientConn.streamShutdown(0, Shutdown.Write, 42); + expect(() => clientConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + + // Can't send + expect(() => + clientConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(() => clientConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Still no change + expect(clientConn.isReadable()).toBeFalse(); + expect(clientConn.streamReadable(0)).toBeFalse(); + expect(clientConn.streamFinished(0)).toBeFalse(); + expect(clientConn.streamWritable(0, 0)).toBeTrue(); + expect(clientConn.streamCapacity(0)).toBeLessThan(12000); + }); + test('manually clean up server stream state', async () => { + serverConn.streamShutdown(0, Shutdown.Read, 42); + expect(() => serverConn.streamShutdown(0, Shutdown.Read, 42)).toThrow( + 'Done', + ); + serverConn.streamShutdown(0, Shutdown.Write, 42); + expect(() => serverConn.streamShutdown(0, Shutdown.Write, 42)).toThrow( + 'Done', + ); + + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + + // No change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); + + // Can't send + expect(() => + serverConn.streamSend(0, Buffer.from('message'), false), + ).toThrow('FinalSize'); + // Can still recv + expect(() => serverConn.streamRecv(0, streamBuf)).toThrow('Done'); + + // Still no change + expect(serverConn.isReadable()).toBeFalse(); + expect(serverConn.streamReadable(0)).toBeFalse(); + expect(serverConn.streamFinished(0)).toBeFalse(); + expect(serverConn.streamWritable(0, 0)).toBeTrue(); + expect(serverConn.streamCapacity(0)).toBeLessThanOrEqual(12000); + }); + test('no new packets', async () => { + // No new packets + expect(() => sendPacket(serverConn, clientConn)).toThrow('Done'); + expect(() => sendPacket(clientConn, serverConn)).toThrow('Done'); + }); + }); + }); + // TODO + // test closing writable from both sides, optional? + // test closing readable from both sides, optional? +}); From 242859cb90752ca1294caf19d068130851e58cb2 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Mon, 3 Jul 2023 14:29:48 +1000 Subject: [PATCH 20/22] fix: refactored and cleaned up - cleaning up `QUICStream.ts` writable stream logic - cleaning up `QUICStream.ts` readable stream logic - fixing up `QUICStream` tests [ci skip] --- src/QUICClient.ts | 7 +- src/QUICStream.ts | 315 ++++------ tests/QUICStream.test.ts | 1267 +++++++++++++++++++------------------- 3 files changed, 763 insertions(+), 826 deletions(-) diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 3320eada..79f045da 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -335,8 +335,13 @@ class QUICClient extends EventTarget { }), ); } + } + if (e instanceof events.QUICConnectionStreamEvent) { + this.dispatchEvent( + new events.QUICConnectionStreamEvent({ detail: e.detail }), + ); } else { - this.dispatchEvent(e); + throw Error('TMP MUST RETHROW EVENTS'); } }; diff --git a/src/QUICStream.ts b/src/QUICStream.ts index 4fd28deb..13df2ab4 100644 --- a/src/QUICStream.ts +++ b/src/QUICStream.ts @@ -15,7 +15,7 @@ import type { import { ReadableStream, WritableStream, - ByteLengthQueuingStrategy, + CountQueuingStrategy, } from 'stream/web'; import Logger from '@matrixai/logger'; import { @@ -56,12 +56,8 @@ class QUICStream protected writableController: WritableStreamDefaultController; protected _sendClosed: boolean = false; protected _recvClosed: boolean = false; - protected _recvPaused: boolean = false; + protected resolveReadableP?: () => void; protected resolveWritableP?: () => void; - // This resolves when `streamSend` would result in a `StreamStopped(u64)` error indicating sending has ended - protected sendFinishedProm = utils.promise(); - // This resolves when `streamRecv` results in a `StreamReset(u64)` or a fin flag indicating receiving has ended - protected recvFinishedProm = utils.promise(); /** * For `reasonToCode`, return 0 means "unknown reason" @@ -129,21 +125,74 @@ class QUICStream this.readable = new ReadableStream( { - type: 'bytes', - // AutoAllocateChunkSize: 1024, + // Type: 'bytes', start: (controller) => { this.readableController = controller; }, - pull: async () => { - this._recvPaused = false; + pull: async (controller) => { + this.logger.warn('attempting data pull'); + // If nothing to read then we wait + if (!this.conn.streamReadable(this.streamId)) { + const readProm = utils.promise(); + this.resolveReadableP = readProm.resolveP; + this.logger.warn('waiting for readable'); + await readProm.p; + } + + const buf = Buffer.alloc(1024); + let recvLength: number, fin: boolean; + try { + [recvLength, fin] = this.conn.streamRecv(this.streamId, buf); + } catch (e) { + if (e.message === 'Done') { + // When it is reported to be `Done`, it just means that there is no data to read + // it does not mean that the stream is closed or finished + // In such a case, we just ignore and continue + // However after the stream is closed, then it would continue to return `Done` + // This can only occur in 2 ways, either via the `fin` + // or through an exception here where the stream reports an error + // Since we don't call this method unless it is readable + // This should never be reported... (this branch should be dead code) + return; + } else { + this.logger.debug(`Stream recv reported: error ${e.message}`); + if (!this._recvClosed) { + const reason = + (await this.processSendStreamError(e, 'recv')) ?? e; + // If it is `StreamReset(u64)` error, then the peer has closed + // the stream, and we are receiving the error code + // If it is not a `StreamReset(u64)`, then something else broke, + // and we need to propagate the error up and down the stream + controller.error(reason); + await this.closeRecv(true, reason); + } + return; + } + } + this.logger.debug(`stream read ${recvLength} bytes with fin(${fin})`); + // If fin is true, then that means, the stream is CLOSED + if (!fin) { + // Send the data normally + if (!this._recvClosed) { + this.readableController.enqueue(buf.subarray(0, recvLength)); + } + } else { + // Strip the end message, removing the null byte + if (!this._recvClosed && recvLength > 1) { + this.readableController.enqueue(buf.subarray(0, recvLength - 1)); + } + await this.closeRecv(); + controller.close(); + return; + } }, cancel: async (reason) => { + this.logger.debug(`readable aborted with [${reason.message}]`); await this.closeRecv(true, reason); }, }, - new ByteLengthQueuingStrategy({ - highWaterMark: 0, - // HighWaterMark: maxReadableStreamBytes, + new CountQueuingStrategy({ + highWaterMark: 1, }), ); @@ -152,9 +201,9 @@ class QUICStream start: (controller) => { this.writableController = controller; }, - write: async (chunk: Uint8Array) => { - await this.streamSend(chunk); - void this.connection.send().catch(() => {}); + write: async (chunk: Uint8Array, controller) => { + await this.streamSend(chunk).catch((e) => controller.error(e)); + await this.connection.send(); }, close: async () => { // This gracefully closes, by sending a message at the end @@ -164,27 +213,23 @@ class QUICStream // But continue to do the below this.logger.debug('sending fin frame'); // This.sendFinishedProm.resolveP(); - await this.streamSend(Buffer.from([0]), true).catch((e) => { - // Ignore send error if stream is already closed - if (e.message !== 'send') throw e; - }); + await this.streamSend(Buffer.from([0]), true); + // Close without error await this.closeSend(); - void this.connection.send().catch(() => {}); }, abort: async (reason?: any) => { // Abort can be called even if there are writes are queued up // The chunks are meant to be thrown away - // We could tell it to shutdown - // This sends a `RESET_STREAM` frame, this abruptly terminates the sending part of a stream + // We could tell it to shut down + // This sends a `RESET_STREAM` frame, this abruptly terminates the writing part of a stream // The receiver can discard any data it already received on that stream // We don't have "unidirectional" streams so that's not important... await this.closeSend(true, reason); }, }, - new ByteLengthQueuingStrategy({ - highWaterMark: 0, - // HighWaterMark: maxWritableStreamBytes, - }), + { + highWaterMark: 1, + }, ); } @@ -196,10 +241,6 @@ class QUICStream return this._recvClosed; } - public get recvPaused(): boolean { - return this._recvPaused; - } - /** * Connection information including hosts, ports and cert data. */ @@ -222,6 +263,10 @@ class QUICStream * * 1. Top-down control flow - means explicit destruction from QUICConnection * 2. Bottom-up control flow - means stream events from users of this stream + * + * This will not wait for any transition events, It's either called when both + * directions have closed. Or when force closing the connection which does not + * require waiting. */ public async destroy({ force = false }: { force?: boolean } = {}) { this.logger.info(`Destroy ${this.constructor.name}`); @@ -235,12 +280,7 @@ class QUICStream this.writableController.error(e); await this.closeSend(true, e); } - void this.connection.send().catch(() => {}); - this.logger.debug('waiting for underlying streams to finish'); - this.isFinished(); - // We need to wait for the connection to finish before fully destroying - await Promise.all([this.sendFinishedProm.p, this.recvFinishedProm.p]); - this.logger.debug('done waiting for underlying streams to finish'); + await this.connection.send(); this.streamMap.delete(this.streamId); this.dispatchEvent(new events.QUICStreamDestroyEvent()); this.logger.info(`Destroyed ${this.constructor.name}`); @@ -262,18 +302,22 @@ class QUICStream } /** - * External push is converted to internal pull - * Internal system decides when to unblock + * Called when stream is present in the `connection.readable` iterator + * Checks for certain close conditions when blocked and closes the web-stream. */ @ready(new errors.ErrorQUICStreamDestroyed(), false, ['destroying']) public read(): void { - // After reading it's possible the writer had a state change. - this.isSendFinished(); - if (this._recvPaused) { - // Do nothing if we are paused - return; + // If we're readable and the readable stream is still waiting, then we need to push some data. + // Or if the stream has finished we need to read and clean up. + this.logger.warn(`desired size ${this.readableController.desiredSize}`); + if ( + (this.readableController.desiredSize != null && + this.readableController.desiredSize > 0) || + this.conn.streamFinished(this.streamId) + ) { + // We resolve the read block + if (this.resolveReadableP != null) this.resolveReadableP(); } - void this.streamRecv().catch(() => {}); } /** @@ -282,144 +326,28 @@ class QUICStream */ @ready(new errors.ErrorQUICStreamDestroyed(), false, ['destroying']) public write(): void { - // Checking if writable has ended - this.isSendFinished(); - if (this.resolveWritableP != null) { - this.resolveWritableP(); - } - } - - /** - * Checks if the underlying stream has finished. - * Will trigger send and recv stream destruction events if so. - */ - public isFinished(): boolean { - return this.isRecvFinished() && this.isSendFinished(); - } - - /** - * Checks if the stream has finished receiving data. - * Will trigger recv close if it has finished. - * Returns if the stream has finished, This can be due to a `fin` or a `RESET_STREAM` frame. - */ - public isRecvFinished(): boolean { - const recvFinished = this.conn.streamFinished(this.streamId); - if (recvFinished) { - // If it is finished then we resolve the promise and clean up - this.recvFinishedProm.resolveP(); - if (!this._recvClosed) { - const err = new errors.ErrorQUICStreamUnexpectedClose( - 'Readable stream closed early with no reason', - ); - this.readableController.error(err); - void this.closeRecv(true, err).catch(() => {}); - } - } - return recvFinished; - } - - /** - * Checks if the stream has finished sending data. - * Will trigger recv close if it has finished. - * This will likely be due to a 'STOP_SENDING' frame. - */ - public isSendFinished(): boolean { + // Checking if the writable had an error try { this.conn.streamWritable(this.streamId, 0); - return false; } catch (e) { - // If the writable has ended, we need to close the writable. - // We need to do this in the background to keep this synchronous. - this.sendFinishedProm.resolveP(); - void this.processSendStreamError(e, 'send').then((reason) => { - if (!this._sendClosed) { - const err = - reason ?? - new errors.ErrorQUICStreamUnexpectedClose( - 'Writable stream closed early with no reason', - ); - this.writableController.error(err); - void this.closeSend(true, err).catch(() => {}); - } - }); - return true; + // If it threw an error, then the stream was closed with an error + // We need to attempt a write to trigger state change and remove stream from writable iterator + void this.streamSend(Buffer.from('dummy data'), true).catch(() => {}); } - } - - protected async streamRecv(): Promise { - const buf = Buffer.alloc(1024); - let recvLength: number, fin: boolean; - while (true) { - try { - [recvLength, fin] = this.conn.streamRecv(this.streamId, buf); - } catch (e) { - if (e.message === 'Done') { - // When it is reported to be `Done`, it just means that there is no data to read - // it does not mean that the stream is closed or finished - // In such a case, we just ignore and continue - // However after the stream is closed, then it would continue to return `Done` - // This can only occur in 2 ways, either via the `fin` - // or through an exception here where the stream reports an error - // Since we don't call this method unless it is readable - // This should never be reported... (this branch should be dead code) - return; - } else { - this.logger.debug(`Stream recv reported: error ${e.message}`); - // Signal receiving has ended - this.recvFinishedProm.resolveP(); - if (!this._recvClosed) { - const reason = await this.processSendStreamError(e, 'recv'); - if (reason != null) { - // If it is `StreamReset(u64)` error, then the peer has closed - // the stream, and we are receiving the error code - this.readableController.error(reason); - await this.closeRecv(true, reason); - } else { - // If it is not a `StreamReset(u64)`, then something else broke - // and we need to propagate the error up and down the stream - this.readableController.error(e); - await this.closeRecv(true, e); - } - } - return; - } - } - // If fin is true, then that means, the stream is CLOSED - if (!fin) { - // Send the data normally - if (!this._recvClosed) { - this.readableController.enqueue(buf.subarray(0, recvLength)); - } - } else { - // Strip the end message, removing the null byte - if (!this._recvClosed && recvLength > 1) { - this.readableController.enqueue(buf.subarray(0, recvLength - 1)); - } - // This will render `stream.cancel` a noop - if (!this._recvClosed) this.readableController.close(); - await this.closeRecv(); - // Signal receiving has ended - this.recvFinishedProm.resolveP(); - return; - } - // Now we pause receiving if the queue is full - if ( - this.readableController.desiredSize != null && - this.readableController.desiredSize <= 0 - ) { - this._recvPaused = true; - } + if (this.resolveWritableP != null) { + this.resolveWritableP(); } } protected async streamSend(chunk: Uint8Array, fin = false): Promise { // This means that the number of written bytes returned can be lower - // than the length of the input buffer when the stream doesn’t have + // than the length of the input buffer when the stream doesn't have // enough capacity for the operation to complete. The application // should retry the operation once the stream is reported as writable again. let sentLength: number; try { sentLength = this.conn.streamSend(this.streamId, chunk, fin); + this.logger.debug(`stream wrote ${sentLength} bytes with fin(${fin})`); } catch (e) { // If the Done is returned // then no data was sent @@ -433,37 +361,33 @@ class QUICStream sentLength = -1; } else { // Signal sending has ended - this.sendFinishedProm.resolveP(); // We may receive a `StreamStopped(u64)` exception // meaning the peer has signalled for us to stop writing // If this occurs, we need to go back to the writable stream // and indicate that there was an error now // Actually it's sufficient to simply throw an exception I think // That would essentially do it - const reason = await this.processSendStreamError(e, 'send'); - if (reason != null) { - // We have to close the send side (but the stream is already closed) - await this.closeSend(true, e); - // Throws the exception back to the writer - throw reason; - } else { - // Something else broke - // here we close the stream by sending a `STREAM_RESET` - // with the error, this doesn't involving calling `streamSend` - await this.closeSend(true, e); - throw e; - } + const reason = (await this.processSendStreamError(e, 'send')) ?? e; + // We have to close the send side (but the stream is already closed) + await this.closeSend(true, reason); + // Throws the exception back to the writer + throw reason; } } if (sentLength < chunk.length) { const { p: writableP, resolveP: resolveWritableP } = utils.promise(); this.resolveWritableP = resolveWritableP; + this.logger.debug( + `stream wrote only ${sentLength}/${chunk.byteLength} bytes, waiting for capacity`, + ); await writableP; // If the `sentLength` is -1, then it will be raised to `0` - return await this.streamSend( - chunk.subarray(Math.max(sentLength, 0)), - fin, + const remainingMessage = chunk.subarray(Math.max(sentLength, 0)); + await this.streamSend(remainingMessage, fin); + this.logger.debug( + `stream wrote remaining ${remainingMessage.byteLength} bytes with fin(${fin})`, ); + return; } } @@ -492,6 +416,7 @@ class QUICStream // Ignore if already shutdown if (e.message !== 'Done') throw e; } + this.readableController.error(reason); } await this.connection.send(); if (this[status] !== 'destroying' && this._recvClosed && this._sendClosed) { @@ -503,8 +428,9 @@ class QUICStream } /** - * This is called from events on the stream - * If `isError` is true, then it will terminate with an reason. + * This is called from events on the stream. + * Will trigger any error and clean up logic events. + * If `isError` is true, then it will terminate with a reason. * The reason is converted to a code, and sent in a `RESET_STREAM` frame. */ protected async closeSend( @@ -517,18 +443,19 @@ class QUICStream this.logger.debug(`Close Send`); // Indicate that the sending side is closed this._sendClosed = true; - // If the QUIC stream is already closed - // there's nothing to do on the QUIC stream - const code = isError ? await this.reasonToCode('send', reason) : 0; - // This will send a `RESET_STREAM` frame with the code - // When the other peer receives, they will get a `StreamReset(u64)` exception if (isError) { try { + // If the QUIC stream is already closed + // there's nothing to do on the QUIC stream + const code = await this.reasonToCode('send', reason); + // This will send a `RESET_STREAM` frame with the code + // When the other peer receives, they will get a `StreamReset(u64)` exception this.conn.streamShutdown(this.streamId, quiche.Shutdown.Write, code); } catch (e) { // Ignore if already shutdown if (e.message !== 'Done') throw e; } + this.writableController.error(reason); } await this.connection.send(); if (this[status] !== 'destroying' && this._recvClosed && this._sendClosed) { @@ -556,7 +483,7 @@ class QUICStream } match = e.message.match(/InvalidStreamState\((.+)\)/); if (match != null) { - // `InvalidStreamState()` returns the stream Id and not any actual error code + // `InvalidStreamState()` returns the stream ID and not any actual error code return await this.codeToReason(type, 0); } return null; diff --git a/tests/QUICStream.test.ts b/tests/QUICStream.test.ts index 14b95d2f..a6e66709 100644 --- a/tests/QUICStream.test.ts +++ b/tests/QUICStream.test.ts @@ -1,6 +1,7 @@ import type * as events from '@/events'; -import type { Crypto, Host, Port } from '@'; +import type { Host, Port } from '@'; import type QUICSocket from '@/QUICSocket'; +import type { ClientCrypto, ServerCrypto } from '@'; import { testProp, fc } from '@fast-check/jest'; import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; import { destroyed } from '@matrixai/async-init'; @@ -9,514 +10,514 @@ import QUICServer from '@/QUICServer'; import QUICClient from '@/QUICClient'; import QUICStream from '@/QUICStream'; import * as testsUtils from './utils'; +import { generateConfig } from './utils'; describe(QUICStream.name, () => { - const logger = new Logger(`${QUICStream.name} Test`, LogLevel.WARN, [ + const logger = new Logger(`${QUICStream.name} Test`, LogLevel.DEBUG, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), ]); + const defaultType = 'RSA'; // This has to be setup asynchronously due to key generation - let crypto: { - key: ArrayBuffer; - ops: Crypto; + const serverCrypto: ServerCrypto = { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + }; + let key: ArrayBuffer; + const clientCrypto: ClientCrypto = { + randomBytes: testsUtils.randomBytes, }; let sockets: Set; // We need to test the stream making beforeEach(async () => { - crypto = { - key: await testsUtils.generateKey(), - ops: { - sign: testsUtils.sign, - verify: testsUtils.verify, - randomBytes: testsUtils.randomBytes, - }, - }; + key = await testsUtils.generateKeyHMAC(); sockets = new Set(); }); afterEach(async () => { const stopProms: Array> = []; for (const socket of sockets) { - stopProms.push(socket.stop(true)); + stopProms.push(socket.stop({ force: true })); } await Promise.allSettled(stopProms); }); - testProp( - 'should create streams', - [tlsConfigWithCaArb], - async (tlsConfigProm) => { - const streamsNum = 10; - const connectionEventProm = - utils.promise(); - const tlsConfig = await tlsConfigProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - let streamCount = 0; - const streamCreationProm = utils.promise(); - conn.addEventListener('stream', () => { - streamCount += 1; - if (streamCount >= streamsNum) streamCreationProm.resolveP(); - }); - // Let's make a new streams. - // const message = Buffer.from('hello!'); - const message = Buffer.from('Hello!'); - for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); - const writer = stream.writable.getWriter(); - await writer.write(message); - writer.releaseLock(); - } - await Promise.race([ - streamCreationProm.p, - testsUtils.sleep(500).then(() => { - throw Error('Creation timed out'); - }), - ]); - expect(streamCount).toBe(streamsNum); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); - testProp( - 'destroying stream should clean up on both ends while streams are used', - [ - tlsConfigWithCaArb, - fc.integer({ min: 5, max: 10 }).noShrink(), - fc.uint8Array({ minLength: 1 }).noShrink(), - ], - async (tlsConfigProm, streamsNum, message) => { - const connectionEventProm = - utils.promise(); - const tlsConfig = await tlsConfigProm; - const streams: Array = []; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - let streamCreatedCount = 0; - let streamEndedCount = 0; - const streamCreationProm = utils.promise(); - const streamEndedProm = utils.promise(); - conn.addEventListener( - 'stream', - (asd: events.QUICConnectionStreamEvent) => { - const stream = asd.detail; - streamCreatedCount += 1; - if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); - void stream.readable - .pipeTo(stream.writable) - // Ignore errors - .catch(() => {}) - .finally(() => { - streamEndedCount += 1; - if (streamEndedCount >= streamsNum) streamEndedProm.resolveP(); - }); - }, - ); - // Let's make a new streams. - for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); - streams.push(stream); - const writer = stream.writable.getWriter(); - await writer.write(message); - writer.releaseLock(); - } - await Promise.race([ - streamCreationProm.p, - testsUtils.sleep(100).then(() => { - throw Error('Creation timed out'); - }), - ]); - // Start destroying streams - await Promise.allSettled(streams.map((stream) => stream.destroy())); - await Promise.race([ - streamEndedProm.p, - testsUtils.sleep(100).then(() => { - throw Error('Ending timed out'); - }), - ]); - expect(streamCreatedCount).toBe(streamsNum); - expect(streamEndedCount).toBe(streamsNum); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); - testProp( - 'should send data over stream', - [ - tlsConfigWithCaArb, - fc - .array(fc.array(fc.uint8Array({ minLength: 1 })), { - minLength: 1, - }) - .noShrink(), - ], - async (tlsConfigProm, streamsData) => { - const connectionEventProm = - utils.promise(); - const tlsConfig = await tlsConfigProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - port: 58888 as Port, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const activeServerStreams: Array> = []; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - activeServerStreams.push(streamProm); - }, - ); + test('should create streams', async () => { + const streamsNum = 10; + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + let streamCount = 0; + const streamCreationProm = utils.promise(); + conn.addEventListener('connectionStream', () => { + streamCount += 1; + if (streamCount >= streamsNum) streamCreationProm.resolveP(); + }); + // Let's make a new streams. + // const message = Buffer.from('hello!'); + const message = Buffer.from('Hello!'); + for (let i = 0; i < streamsNum; i++) { + const stream = await client.connection.streamNew(); + const writer = stream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + } + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(500).then(() => { + throw Error('Creation timed out'); + }), + ]); + expect(streamCount).toBe(streamsNum); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); + test('destroying stream should clean up on both ends while streams are used', async () => { + const message = Buffer.from('Message!'); + const streamsNum = 10; + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const streams: Array = []; + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + let streamCreatedCount = 0; + let streamEndedCount = 0; + const streamCreationProm = utils.promise(); + const streamEndedProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (event: events.QUICConnectionStreamEvent) => { + const stream = event.detail; + streamCreatedCount += 1; + if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); + void stream.readable + .pipeTo(stream.writable) + // Ignore errors + .catch(() => {}) + .finally(() => { + streamEndedCount += 1; + if (streamEndedCount >= streamsNum) streamEndedProm.resolveP(); + }); + }, + ); + // Let's make a new streams. + for (let i = 0; i < streamsNum; i++) { + const stream = await client.connection.streamNew(); + streams.push(stream); + const writer = stream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + } + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(100).then(() => { + throw Error('Creation timed out'); + }), + ]); + // Start destroying streams + await Promise.allSettled( + streams.map((stream) => stream.destroy({ force: true })), + ); + await Promise.race([ + streamEndedProm.p, + testsUtils.sleep(100000).then(() => { + throw Error('Ending timed out'); + }), + ]); + expect(streamCreatedCount).toBe(streamsNum); + expect(streamEndedCount).toBe(streamsNum); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); + test('should send data over stream', async () => { + const message = Buffer.from('The Quick Brown Fox Jumped Over The Lazy Dog'); + const numStreams = 10; + const numMessage = 100; + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + port: 58888 as Port, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const activeServerStreams: Array> = []; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable.pipeTo(stream.writable); + activeServerStreams.push(streamProm); + }, + ); - // Let's make a new streams. - const activeClientStreams: Array> = []; - for (const data of streamsData) { - activeClientStreams.push( - (async () => { - const stream = await client.connection.streamNew(); - const writer = stream.writable.getWriter(); - const reader = stream.readable.getReader(); - // Do write and read messages here. - for (const message of data) { - await writer.write(message); - const readMessage = await reader.read(); - expect(readMessage.done).toBeFalse(); - expect(readMessage.value).toStrictEqual(message); - } - await writer.close(); - const value = await reader.read(); - expect(value.done).toBeTrue(); - })(), - ); - } - await Promise.all([ - Promise.all(activeClientStreams), - Promise.all(activeServerStreams), - ]); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); - testProp( - 'should propagate errors over stream for writable', - [tlsConfigWithCaArb, fc.integer({ min: 1, max: 10 }).noShrink()], - async (tlsConfigProm, streamsNum) => { - const testReason = Symbol('TestReason'); - const codeToReason = (type, code) => { - switch (code) { - case 2: - return testReason; - default: - return new Error(`${type.toString()} ${code.toString()}`); - } - }; - const reasonToCode = (type, reason) => { - if (reason === testReason) return 2; - return 1; - }; - const connectionEventProm = - utils.promise(); - const tlsConfig = await tlsConfigProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - codeToReason, - reasonToCode, - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - port: 59999 as Port, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - codeToReason, - reasonToCode, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const activeServerStreams: Array> = []; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable.pipeTo(stream.writable); - // Ignore unhandled errors - streamProm.catch(() => {}); - activeServerStreams.push(streamProm); - }, - ); - // Let's make a new streams. - const activeClientStreams: Array> = []; - const message = Buffer.from('Hello!'); - for (let i = 0; i < streamsNum; i++) { - activeClientStreams.push( - (async () => { - const stream = await client.connection.streamNew(); - const writer = stream.writable.getWriter(); - // Do write and read messages here. + // Let's make a new streams. + const activeClientStreams: Array> = []; + for (let i = 0; i < numStreams; i++) { + activeClientStreams.push( + (async () => { + const stream = await client.connection.streamNew(); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + // Do write and read messages here. + for (let j = 0; j < numMessage; j++) { await writer.write(message); - await writer.abort(testReason); - try { - for await (const _ of stream.readable) { - // Do nothing, wait for finish - } - } catch (e) { - expect(e).toBe(testReason); - } - })(), - ); - } - const expectationProms = activeServerStreams.map(async (v) => { - await v.catch((e) => { - expect(e).toBe(testReason); - }); - }); - await Promise.all([ - Promise.all(activeClientStreams), - Promise.all(expectationProms), - ]); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); - testProp.skip( - 'should propagate errors over stream for readable', - [tlsConfigWithCaArb, fc.integer({ min: 5, max: 5 }).noShrink()], - async (tlsConfigProm, streamsNum) => { - const testReason = Symbol('TestReason'); - const codeToReason = (type, code) => { - switch (code) { - case 2: - return testReason; - default: - return new Error(`${type.toString()} ${code.toString()}`); - } - }; - const reasonToCode = (type, reason) => { - if (reason === testReason) return 2; - return 1; - }; - const connectionEventProm = - utils.promise(); - const tlsConfig = await tlsConfigProm; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - codeToReason, - reasonToCode, - config: { - tlsConfig: tlsConfig.tlsConfig, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), + const readMessage = await reader.read(); + expect(readMessage.done).toBeFalse(); + expect(readMessage.value).toStrictEqual(message); + } + await writer.close(); + const value = await reader.read(); + expect(value.done).toBeTrue(); + })(), ); - await server.start({ - host: '127.0.0.1' as Host, - port: 60000 as Port, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - codeToReason, - reasonToCode, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const activeServerStreams: Array> = []; - const serverStreamsProm = utils.promise(); - let serverStreamNum = 0; - conn.addEventListener( - 'stream', - (streamEvent: events.QUICConnectionStreamEvent) => { - const stream = streamEvent.detail; - const streamProm = stream.readable - .pipeTo(stream.writable) - .catch((e) => { - expect(e).toBe(testReason); - }); - activeServerStreams.push(streamProm); - serverStreamNum += 1; - if (serverStreamNum >= streamsNum) serverStreamsProm.resolveP(); - }, - ); - // Let's make a new streams. - const activeClientStreams: Array> = []; - const message = Buffer.from('Hello!'); - const serverStreamsDoneProm = utils.promise(); - for (let i = 0; i < streamsNum; i++) { - const clientProm = (async () => { + } + await Promise.all([ + Promise.all(activeClientStreams), + Promise.all(activeServerStreams), + ]); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); + test('should propagate errors over stream for writable', async () => { + const streamsNum = 10; + const testReason = Symbol('TestReason'); + const codeToReason = (type, code) => { + switch (code) { + case 2: + return testReason; + default: + return new Error(`${type.toString()} ${code.toString()}`); + } + }; + const reasonToCode = (type, reason) => { + if (reason === testReason) return 2; + return 1; + }; + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + codeToReason, + reasonToCode, + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + port: 59999 as Port, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + codeToReason, + reasonToCode, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const activeServerStreams: Array> = []; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable.pipeTo(stream.writable); + // Ignore unhandled errors + streamProm.catch(() => {}); + activeServerStreams.push(streamProm); + }, + ); + // Let's make a new streams. + const activeClientStreams: Array> = []; + const message = Buffer.from('Hello!'); + for (let i = 0; i < streamsNum; i++) { + activeClientStreams.push( + (async () => { const stream = await client.connection.streamNew(); const writer = stream.writable.getWriter(); // Do write and read messages here. await writer.write(message); - await stream.readable.cancel(testReason); - await serverStreamsDoneProm.p; - // Need time for packets to send/recv - await testsUtils.sleep(100); - const writeProm = writer.write(message); - await writeProm.then( - () => { - throw Error('write did not throw'); - }, - (e) => expect(e).toBe(testReason), - ); - })(); - // ClientProm.catch(e => logger.error(e)); - activeClientStreams.push(clientProm); + await writer.abort(testReason); + try { + for await (const _ of stream.readable) { + // Do nothing, wait for finish + } + } catch (e) { + expect(e).toBe(testReason); + } + })(), + ); + } + const expectationProms = activeServerStreams.map(async (v) => { + await v.catch((e) => { + expect(e).toBe(testReason); + }); + }); + await Promise.all([ + Promise.all(activeClientStreams), + Promise.all(expectationProms), + ]); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); + test('should propagate errors over stream for readable', async () => { + const streamsNum = 1; + const testReason = Symbol('TestReason'); + const codeToReason = (type, code) => { + switch (code) { + case 2: + return testReason; + default: + return new Error(`${type.toString()} ${code.toString()}`); } - // Wait for streams to be created before mapping - await serverStreamsProm.p; - await Promise.all([ - Promise.all(activeClientStreams), - Promise.all(activeServerStreams).finally(() => { - serverStreamsDoneProm.resolveP(); - }), - ]); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); + }; + const reasonToCode = (type, reason) => { + if (reason === testReason) return 2; + return 1; + }; + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + codeToReason, + reasonToCode, + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + port: 60000 as Port, + }); + const client = await QUICClient.createQUICClient({ + host: '::ffff:127.0.0.1' as Host, + port: server.port, + localHost: '::' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + codeToReason, + reasonToCode, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const activeServerStreams: Array> = []; + const serverStreamsProm = utils.promise(); + let serverStreamNum = 0; + conn.addEventListener( + 'connectionStream', + (streamEvent: events.QUICConnectionStreamEvent) => { + const stream = streamEvent.detail; + const streamProm = stream.readable + .pipeTo(stream.writable) + .catch((e) => { + expect(e).toBe(testReason); + }); + activeServerStreams.push(streamProm); + serverStreamNum += 1; + if (serverStreamNum >= streamsNum) serverStreamsProm.resolveP(); + }, + ); + // Let's make a new streams. + const activeClientStreams: Array> = []; + const message = Buffer.from('Hello!'); + const serverStreamsDoneProm = utils.promise(); + for (let i = 0; i < streamsNum; i++) { + const clientProm = (async () => { + const stream = await client.connection.streamNew(); + const writer = stream.writable.getWriter(); + // Do write and read messages here. + await writer.write(message); + await stream.readable.cancel(testReason); + await serverStreamsDoneProm.p; + // Need time for packets to send/recv + await testsUtils.sleep(100); + const writeProm = writer.write(message); + await writeProm.then( + () => { + throw Error('write did not throw'); + }, + (e) => expect(e).toBe(testReason), + ); + })(); + // ClientProm.catch(e => logger.error(e)); + activeClientStreams.push(clientProm); + } + // Wait for streams to be created before mapping + await serverStreamsProm.p; + await Promise.all([ + Promise.all(activeClientStreams), + Promise.all(activeServerStreams).finally(() => { + serverStreamsDoneProm.resolveP(); + }), + ]); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); testProp( 'should clean up streams when connection ends', [ - tlsConfigWithCaArb, fc.integer({ min: 5, max: 10 }).noShrink(), fc.uint8Array({ minLength: 1 }).noShrink(), ], - async (tlsConfigProm, streamsNum, message) => { + async (streamsNum, message) => { const connectionEventProm = utils.promise(); - const tlsConfig = await tlsConfigProm; + const tlsConfig = await generateConfig(defaultType); const server = new QUICServer({ - crypto, + crypto: { + key, + ops: serverCrypto, + }, logger: logger.getChild(QUICServer.name), config: { - tlsConfig: tlsConfig.tlsConfig, + key: tlsConfig.key, + cert: tlsConfig.cert, verifyPeer: false, }, }); testsUtils.extractSocket(server, sockets); server.addEventListener( - 'connection', + 'serverConnection', (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); @@ -527,7 +528,9 @@ describe(QUICStream.name, () => { host: '::ffff:127.0.0.1' as Host, port: server.port, localHost: '::' as Host, - crypto, + crypto: { + ops: clientCrypto, + }, logger: logger.getChild(QUICClient.name), config: { verifyPeer: false, @@ -541,7 +544,7 @@ describe(QUICStream.name, () => { const streamCreationProm = utils.promise(); const streamEndedProm = utils.promise(); conn.addEventListener( - 'stream', + 'connectionStream', (asd: events.QUICConnectionStreamEvent) => { const stream = asd.detail; streamCreatedCount += 1; @@ -584,170 +587,172 @@ describe(QUICStream.name, () => { }, { numRuns: 2 }, ); - testProp( - 'streams should contain metadata', - [tlsConfigWithCaGENOKPArb, tlsConfigWithCaGENOKPArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const connectionEventProm = - utils.promise(); - const tlsConfig1 = await tlsConfigProm1; - const tlsConfig2 = await tlsConfigProm2; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig1.tlsConfig, - verifyPeer: true, - verifyPem: tlsConfig2.ca.certChainPem, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, - port: server.port, - localHost: '127.0.0.1' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - tlsConfig: tlsConfig2.tlsConfig, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const serverStreamProm = utils.promise(); - conn.addEventListener( - 'stream', - (event: events.QUICConnectionStreamEvent) => { - serverStreamProm.resolveP(event.detail); - }, - ); - // Let's make a new streams. - const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); - const writer = clientStream.writable.getWriter(); - await writer.write(message); - writer.releaseLock(); - await Promise.race([ - serverStreamProm.p, - testsUtils.sleep(500).then(() => { - throw Error('Creation timed out'); - }), - ]); - const clientMetadata = clientStream.remoteInfo; - expect(clientMetadata.localHost).toBe(client.host); - expect(clientMetadata.localPort).toBe(client.port); - expect(clientMetadata.remoteHost).toBe(server.host); - expect(clientMetadata.remotePort).toBe(server.port); - expect(clientMetadata.remoteCertificates?.length).toBeGreaterThan(0); - const clientPemChain = utils.certificatePEMsToCertChainPem( - clientMetadata.remoteCertificates!, - ); - expect(clientPemChain).toEqual(tlsConfig1.tlsConfig.certChainPem); + test('streams should contain metadata', async () => { + const connectionEventProm = + utils.promise(); + const tlsConfig1 = await generateConfig(defaultType); + const tlsConfig2 = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig1.key, + cert: tlsConfig1.cert, + verifyPeer: true, + ca: tlsConfig2.ca, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '127.0.0.1' as Host, + port: server.port, + localHost: '127.0.0.1' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + key: tlsConfig2.key, + cert: tlsConfig2.cert, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const serverStreamProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (event: events.QUICConnectionStreamEvent) => { + serverStreamProm.resolveP(event.detail); + }, + ); + // Let's make a new streams. + const message = Buffer.from('Hello!'); + const clientStream = await client.connection.streamNew(); + const writer = clientStream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + await Promise.race([ + serverStreamProm.p, + testsUtils.sleep(500).then(() => { + throw Error('Creation timed out'); + }), + ]); + const clientMetadata = clientStream.remoteInfo; + expect(clientMetadata.localHost).toBe(client.host); + expect(clientMetadata.localPort).toBe(client.port); + expect(clientMetadata.remoteHost).toBe(server.host); + expect(clientMetadata.remotePort).toBe(server.port); + expect(clientMetadata.remoteCertificates?.length).toBeGreaterThan(0); + const clientPemChain = utils.certificatePEMsToCertChainPem( + clientMetadata.remoteCertificates!, + ); + expect(clientPemChain).toEqual(tlsConfig1.ca); - const serverStream = await serverStreamProm.p; - const serverMetadata = serverStream.remoteInfo; - expect(serverMetadata.localHost).toBe(server.host); - expect(serverMetadata.localPort).toBe(server.port); - expect(serverMetadata.remoteHost).toBe(client.host); - expect(serverMetadata.remotePort).toBe(client.port); - expect(serverMetadata.remoteCertificates?.length).toBeGreaterThan(0); - const serverPemChain = utils.certificatePEMsToCertChainPem( - serverMetadata.remoteCertificates!, - ); - expect(serverPemChain).toEqual(tlsConfig2.tlsConfig.certChainPem); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 1 }, - ); - testProp( - 'streams can be cancelled', - [tlsConfigWithCaGENOKPArb, tlsConfigWithCaGENOKPArb], - async (tlsConfigProm1, tlsConfigProm2) => { - const cancelReason = Symbol('CancelReason'); - const connectionEventProm = - utils.promise(); - const tlsConfig1 = await tlsConfigProm1; - const tlsConfig2 = await tlsConfigProm2; - const server = new QUICServer({ - crypto, - logger: logger.getChild(QUICServer.name), - config: { - tlsConfig: tlsConfig1.tlsConfig, - verifyPeer: true, - verifyPem: tlsConfig2.ca.certChainPem, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'connection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, - port: server.port, - localHost: '127.0.0.1' as Host, - crypto, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - tlsConfig: tlsConfig2.tlsConfig, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - const serverStreamProm = utils.promise(); - conn.addEventListener( - 'stream', - (event: events.QUICConnectionStreamEvent) => { - serverStreamProm.resolveP(event.detail); - }, - ); - // Let's make a new streams. - const message = Buffer.from('Hello!'); - const clientStream = await client.connection.streamNew(); - const writer = clientStream.writable.getWriter(); - await writer.write(message); - writer.releaseLock(); - await Promise.race([ - serverStreamProm.p, - testsUtils.sleep(500).then(() => { - throw Error('Creation timed out'); - }), - ]); - clientStream.cancel(cancelReason); - await expect(clientStream.readable.getReader().read()).rejects.toBe( - cancelReason, - ); - await expect(clientStream.writable.getWriter().write()).rejects.toBe( - cancelReason, - ); - // Let's check that the server side ended - const serverStream = await serverStreamProm.p; - await expect( - serverStream.readable.pipeTo(serverStream.writable), - ).rejects.toThrow(); - // And client stream should've cleaned up - await testsUtils.sleep(100); - expect(clientStream[destroyed]).toBeTrue(); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); + const serverStream = await serverStreamProm.p; + const serverMetadata = serverStream.remoteInfo; + expect(serverMetadata.localHost).toBe(server.host); + expect(serverMetadata.localPort).toBe(server.port); + expect(serverMetadata.remoteHost).toBe(client.host); + expect(serverMetadata.remotePort).toBe(client.port); + expect(serverMetadata.remoteCertificates?.length).toBeGreaterThan(0); + const serverPemChain = utils.certificatePEMsToCertChainPem( + serverMetadata.remoteCertificates!, + ); + expect(serverPemChain).toEqual(tlsConfig2.ca); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); + test('streams can be cancelled', async () => { + const cancelReason = Symbol('CancelReason'); + const connectionEventProm = + utils.promise(); + const tlsConfig1 = await generateConfig(defaultType); + const tlsConfig2 = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig1.key, + cert: tlsConfig1.cert, + verifyPeer: true, + ca: tlsConfig2.ca, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: '127.0.0.1' as Host, + }); + const client = await QUICClient.createQUICClient({ + host: '127.0.0.1' as Host, + port: server.port, + localHost: '127.0.0.1' as Host, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + key: tlsConfig2.key, + cert: tlsConfig2.cert, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const serverStreamProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (event: events.QUICConnectionStreamEvent) => { + serverStreamProm.resolveP(event.detail); + }, + ); + // Let's make a new streams. + const message = Buffer.from('Hello!'); + const clientStream = await client.connection.streamNew(); + const writer = clientStream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + await Promise.race([ + serverStreamProm.p, + testsUtils.sleep(500).then(() => { + throw Error('Creation timed out'); + }), + ]); + clientStream.cancel(cancelReason); + await expect(clientStream.readable.getReader().read()).rejects.toBe( + cancelReason, + ); + await expect(clientStream.writable.getWriter().write()).rejects.toBe( + cancelReason, + ); + // Let's check that the server side ended + const serverStream = await serverStreamProm.p; + await expect( + serverStream.readable.pipeTo(serverStream.writable), + ).rejects.toThrow(); + // And client stream should've cleaned up + await testsUtils.sleep(100); + expect(clientStream[destroyed]).toBeTrue(); + await client?.destroy({ force: true }); + await server?.stop({ force: true }); + }); }); From ed6f3a3a5f59592ac1bfc1cfe5533c794648e89f Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Mon, 3 Jul 2023 18:10:48 +1000 Subject: [PATCH 21/22] fix: general logic fixes and clean up - logic fixes for early stream ending. - fixing up tests. - fixed up remote info for stream. - fixed problem with process being held open and handling socket errors. - propagating all events to the top level client or server. - cleaning up comments and commentary. - fixed up benchmark. --- benches/stream_1KB.ts | 31 +- src/QUICClient.ts | 39 +- src/QUICConnection.ts | 548 +++++----------- src/QUICServer.ts | 103 ++- src/QUICSocket.ts | 2 - src/QUICStream.ts | 261 ++++---- src/bin/server.ts | 13 +- src/errors.ts | 16 +- src/events.ts | 7 - src/native/napi/config.rs | 2 +- src/types.ts | 2 +- tests/QUICClient.test.ts | 3 +- tests/QUICStream.test.ts | 610 ++++++++++++++---- tests/concurrency.test.ts | 12 +- .../quiche.connection.lifecycle.test.ts | 7 + tests/native/quiche.tls.test.ts | 28 + tests/utils.ts | 3 +- 17 files changed, 950 insertions(+), 737 deletions(-) diff --git a/benches/stream_1KB.ts b/benches/stream_1KB.ts index 0799dc0d..ef5578f7 100644 --- a/benches/stream_1KB.ts +++ b/benches/stream_1KB.ts @@ -16,15 +16,6 @@ async function main() { ), ]); // Setting up initial state - const crypto = { - key: await testsUtils.generateKey(), - ops: { - sign: testsUtils.sign, - verify: testsUtils.verify, - randomBytes: testsUtils.randomBytes, - }, - }; - const data1KiB = Buffer.alloc(1024, 0xf0); const host = '127.0.0.1' as Host; const certChainPem = await fs.promises.readFile( @@ -36,14 +27,18 @@ async function main() { const quicServer = new QUICServer({ config: { - tlsConfig: { - privKeyPem: privKeyPem.toString(), - certChainPem: certChainPem.toString(), - }, + key: privKeyPem.toString(), + cert: certChainPem.toString(), verifyPeer: false, + keepAliveIntervalTime: 1000, + }, + crypto: { + key: await testsUtils.generateKeyHMAC(), + ops: { + sign: testsUtils.signHMAC, + verify: testsUtils.verifyHMAC, + }, }, - keepaliveIntervalTime: 1000, - crypto, logger, }); quicServer.addEventListener( @@ -80,7 +75,11 @@ async function main() { host, port: quicServer.port, localHost: host, - crypto, + crypto: { + ops: { + randomBytes: testsUtils.randomBytes, + }, + }, logger, }); diff --git a/src/QUICClient.ts b/src/QUICClient.ts index 79f045da..59199ab8 100644 --- a/src/QUICClient.ts +++ b/src/QUICClient.ts @@ -28,11 +28,11 @@ import QUICConnection from './QUICConnection'; import QUICConnectionId from './QUICConnectionId'; /** - * You must provide a error handler `addEventListener('error')`. - * Otherwise errors will just be ignored. + * You must provide an error handler `addEventListener('error')`. + * Otherwise, errors will just be ignored. * * Use the same event names. - * However it needs to bubble up. + * However, it needs to bubble up. * And the right target needs to be used. * * Events: @@ -63,7 +63,7 @@ class QUICClient extends EventTarget { * @param opts * @param opts.host - peer host where `0.0.0.0` becomes `127.0.0.1` and `::` becomes `::1` * @param opts.port - * @param opts.localHost - defaults to `::` (dualstack) + * @param opts.localHost - defaults to `::` (dual-stack) * @param opts.localPort - defaults 0 * @param opts.crypto - client only needs the ability to generate random bytes * @param opts.config - optional config @@ -140,7 +140,7 @@ class QUICClient extends EventTarget { await crypto.ops.randomBytes(scidBuffer); const scid = new QUICConnectionId(scidBuffer); let [host_] = await utils.resolveHost(host, resolveHostname); - // If the target host is in fact an zero IP, it cannot be used + // If the target host is in fact a zero IP, it cannot be used // as a target host, so we need to resolve it to a non-zero IP // in this case, 0.0.0.0 is resolved to 127.0.0.1 and :: and ::0 is // resolved to ::1 @@ -227,7 +227,7 @@ class QUICClient extends EventTarget { try { await Promise.race([connectionProm, socketErrorP]); } catch (e) { - // In case the `connection.start` is on-going, we need to abort it + // In case the `connection.start` is ongoing, we need to abort it abortController.abort(e); if (!isSocketShared) { // Stop is idempotent @@ -254,13 +254,13 @@ class QUICClient extends EventTarget { /** * This must not throw any exceptions. */ - protected handleQUICSocketEvents = async (e: events.QUICSocketEvent) => { - if (e instanceof events.QUICSocketErrorEvent) { + protected handleQUICSocketEvents = async (event: events.QUICSocketEvent) => { + if (event instanceof events.QUICSocketErrorEvent) { // QUIC socket errors are re-emitted but a destroy takes place this.dispatchEvent( new events.QUICClientErrorEvent({ detail: new errors.ErrorQUICClient('Socket error', { - cause: e.detail, + cause: event.detail, }), }), ); @@ -276,7 +276,7 @@ class QUICClient extends EventTarget { }), ); } - } else if (e instanceof events.QUICSocketStopEvent) { + } else if (event instanceof events.QUICSocketStopEvent) { // If a QUIC socket stopped, we immediately destroy // However, the stop will have its own constraints try { @@ -292,7 +292,7 @@ class QUICClient extends EventTarget { ); } } else { - this.dispatchEvent(e); + this.dispatchEvent(event); } }; @@ -300,13 +300,13 @@ class QUICClient extends EventTarget { * This must not throw any exceptions. */ protected handleQUICConnectionEvents = async ( - e: events.QUICConnectionEvent, + event: events.QUICConnectionEvent, ) => { - if (e instanceof events.QUICConnectionErrorEvent) { + if (event instanceof events.QUICConnectionErrorEvent) { this.dispatchEvent( new events.QUICClientErrorEvent({ detail: new errors.ErrorQUICClient('Connection error', { - cause: e.detail, + cause: event.detail, }), }), ); @@ -322,7 +322,7 @@ class QUICClient extends EventTarget { }), ); } - } else if (e instanceof events.QUICConnectionStopEvent) { + } else if (event instanceof events.QUICConnectionStopEvent) { try { // Force destroy means don't destroy gracefully await this.destroy({ @@ -335,13 +335,14 @@ class QUICClient extends EventTarget { }), ); } - } - if (e instanceof events.QUICConnectionStreamEvent) { + } else if (event instanceof events.QUICConnectionStreamEvent) { this.dispatchEvent( - new events.QUICConnectionStreamEvent({ detail: e.detail }), + new events.QUICConnectionStreamEvent({ detail: event.detail }), ); + } else if (event instanceof events.QUICStreamDestroyEvent) { + this.dispatchEvent(new events.QUICStreamDestroyEvent()); } else { - throw Error('TMP MUST RETHROW EVENTS'); + utils.never(); } }; diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index 335aa0d9..a9982194 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -31,6 +31,8 @@ import * as utils from './utils'; import { never, promise } from './utils'; import * as errors from './errors'; +const timerCleanupReasonSymbol = Symbol('timerCleanupReasonSymbol'); + /** * Think of this as equivalent to `net.Socket`. * Errors here are emitted to the connection only. @@ -112,18 +114,20 @@ class QUICConnection extends EventTarget { /** * Client initiated unidirectional stream starts at 2. * Increment by 4 to get the next ID. + * Currently unsupported. */ - protected streamIdClientUni: StreamId = 0b10 as StreamId; + protected _streamIdClientUni: StreamId = 0b10 as StreamId; /** * Server initiated unidirectional stream starts at 3. * Increment by 4 to get the next ID. + * Currently unsupported. */ - protected streamIdServerUni: StreamId = 0b11 as StreamId; + protected _streamIdServerUni: StreamId = 0b11 as StreamId; /** * Internal conn timer. This is used to tick the state transitions on the - * conn. + * connection. */ protected connTimeOutTimer?: Timer; @@ -136,7 +140,7 @@ class QUICConnection extends EventTarget { * connection alive if there is no activity. This keep alive mechanism will * trigger ping frames to ensure that there is connection activity. * If the max idle time is set to 0, the connection never times out on idleness. - * However this keep alive mechanism will continue to work in case you need + * However, this keep alive mechanism will continue to work in case you need * activity on the connection for some reason. * Note that the timer used for the `ContextTimed` in `QUICClient.createQUICClient` * is independent of the max idle time. This keep alive mechanism will only @@ -154,12 +158,11 @@ class QUICConnection extends EventTarget { */ protected _remotePort: Port; - // TODO: use it or loose it? /** - * Bubble up all QUIC stream events. + * Bubble up stream destroy event */ - protected handleQUICStreamEvents = (e: events.QUICStreamEvent) => { - this.dispatchEvent(e); + protected handleQUICStreamDestroyEvent = () => { + this.dispatchEvent(new events.QUICStreamDestroyEvent()); }; /** @@ -186,8 +189,6 @@ class QUICConnection extends EventTarget { */ protected closedP: Promise; - protected wasEstablished: boolean = false; - protected resolveEstablishedP: () => void; protected rejectEstablishedP: (reason?: any) => void; protected resolveSecureEstablishedP: () => void; @@ -195,14 +196,11 @@ class QUICConnection extends EventTarget { protected resolveClosedP: () => void; protected rejectClosedP: (reason?: any) => void; - protected lastErrorMessage?: string; - public readonly lockbox = new LockBox(); - public readonly lockCode = 'Lock'; // TODO: more unique code + public readonly lockCode = 'ConnectionEventLockId'; protected customVerified = false; protected shortReceived = false; - protected shortSent = false; protected secured = false; protected count = 0; protected verifyCallback: VerifyCallback | undefined; @@ -288,9 +286,11 @@ class QUICConnection extends EventTarget { abortProm.p, ]); } catch (e) { + const code = + args.reasonToCode != null ? (await args.reasonToCode(e)) ?? 0 : 0; await connection.stop({ applicationError: false, - errorCode: 42, // FIXME: use a proper code + errorCode: code, errorMessage: e.message, force: true, }); @@ -413,22 +413,9 @@ class QUICConnection extends EventTarget { rejectP: rejectEstablishedP, } = utils.promise(); this.establishedP = establishedP; - this.resolveEstablishedP = () => { - // Upon the first time you call this, it is now true - // But prior to this - - this.wasEstablished = true; - resolveEstablishedP(); - }; + this.resolveEstablishedP = resolveEstablishedP; this.rejectEstablishedP = rejectEstablishedP; - // We can do something where, once you "resolve" - // Then it is in fact established - // Alternatively, we can just bind into the `establishedP` here - // Does this become a dangling promsie? - // Cause it is `void` here - // I think it's better to use the `resolveEstablishedP` - const { p: secureEstablishedP, resolveP: resolveSecureEstablishedP, @@ -483,8 +470,12 @@ class QUICConnection extends EventTarget { * `ConnectionErrorCode`. * The default `applicationError` is true because a normal graceful close * is an application error. - * The default `errorCode` of 0 means no error or general error. + * The default `errorCode` of 0 means general error. * This is the same as basically waiting for `closedP`. + * + * Providing error details is only used if the connection still needs to be + * closed. If stop was triggered internally then the error details are obtained + * by the connection. */ public async stop( { @@ -509,60 +500,61 @@ class QUICConnection extends EventTarget { ) { this.logger.info(`Stop ${this.constructor.name}`); // Cleaning up existing streams - const streamDestroyPs: Array> = []; + const streamsDestroyP: Array> = []; + this.logger.debug('triggering stream destruction'); for (const stream of this.streamMap.values()) { - streamDestroyPs.push(stream.destroy({ force })); + // If we're draining then streams will never end on their own. + // We must force them to end. + if (this.conn.isDraining() || this.conn.isClosed() || force) { + await stream.destroy(); + } + streamsDestroyP.push(stream.destroyedP); } - await Promise.all(streamDestroyPs); - // Do we do this afterward or before? - // FIXME: not sure it really matters, don't really need a keep alive while stopping. + this.logger.debug('waiting for streams to destroy'); + await Promise.all(streamsDestroyP); + this.logger.debug('streams destroyed'); this.stopKeepAliveIntervalTimer(); - try { - mon = mon ?? new Monitor(this.lockbox, RWLockWriter); - // Trigger closing connection in the background and await close later. - void mon.withF(this.lockCode, async (mon) => { - // If this is already closed, then `Done` will be thrown - // Otherwise it can send `CONNECTION_CLOSE` frame - // This can be 0x1c close at the QUIC layer or no errors - // Or it can be 0x1d for application close with an error - // Upon receiving a `CONNECTION_CLOSE`, you can send back - // 1 packet containing a `CONNECTION_CLOSE` frame too - // (with `NO_ERROR` code if appropriate) - // It must enter into a draining state, and no other packets can be sent - try { - this.conn.close( - applicationError, - errorCode, - Buffer.from(errorMessage), - ); - // If we get a `Done` exception we don't bother calling send - // The send only gets sent if the `Done` is not the case - await this.send(mon); - } catch (e) { - // Ignore 'Done' if already closed - if (e.message !== 'Done') throw e; + + mon = mon ?? new Monitor(this.lockbox, RWLockWriter); + // Trigger closing connection in the background and await close later. + void mon.withF(this.lockCode, async (mon) => { + // If this is already closed, then `Done` will be thrown + // Otherwise it can send `CONNECTION_CLOSE` frame + // This can be 0x1c close at the QUIC layer or no errors + // Or it can be 0x1d for application close with an error + // Upon receiving a `CONNECTION_CLOSE`, you can send back + // 1 packet containing a `CONNECTION_CLOSE` frame too + // (with `NO_ERROR` code if appropriate) + // It must enter into a draining state, and no other packets can be sent + try { + this.conn.close(applicationError, errorCode, Buffer.from(errorMessage)); + // If we get a `Done` exception we don't bother calling send + // The send only gets sent if the `Done` is not the case + await this.send(mon); + } catch (e) { + // Ignore 'Done' if already closed + if (e.message !== 'Done') { + // No other exceptions are expected + never(); } - }); - } catch (e) { - // If the connection is already closed, `Done` will be thrown - if (e.message !== 'Done') { - // No other exceptions are expected - utils.never(); } - } + }); if (this.conn.isClosed()) { this.resolveClosedP(); } + this.setConnTimeOutTimer(); // Now we await for the closedP + this.logger.debug('awaiting closedP'); await this.closedP; + this.logger.debug('closedP'); + this.connTimeOutTimer?.cancel(timerCleanupReasonSymbol); - // The reason we only delete afterwards - // Is because we do it before we are opened (or just constructed) - // Techincally it was constructed, and then we added ourselves to it - // But during `start` we are just waiting + // Removing the connection from the socket's connection map this.socket.connectionMap.delete(this.connectionId); + // Checking for errors and emitting them as events + // Emit error if connection timed out if (this.conn.isTimedOut()) { const error = this.secured ? new errors.ErrorQUICConnectionIdleTimeOut() @@ -599,6 +591,7 @@ class QUICConnection extends EventTarget { ); } + // Emit error if local error const localError = this.conn.localError(); if (localError != null) { const message = `connection failed with localError ${Buffer.from( @@ -678,17 +671,12 @@ class QUICConnection extends EventTarget { // This may mutate `data` this.conn.recv(data, recvInfo); } catch (e) { + // Should only be a `TLSFail` if we fail here. + // The error details will be available as a local error. if (e.message !== 'TlsFail') { // No other exceptions are expected utils.never(); } - // Do note that if we get a TlsFail - // We must proceed without throwing any exceptions - // But we must "save" the error here - // We don't need the save the localError or remoteError - // Cause that will be "saved" - // Only this e.message <- this will be whatever is the last message - this.lastErrorMessage = e.message; } // Checking if the packet was a short frame. @@ -700,17 +688,13 @@ class QUICConnection extends EventTarget { this.shortReceived = true; } } - - if ( - !this.secured && - this.shortReceived && - this.shortReceived && - !this.conn.isDraining() - ) { + // Checks if `secureEstablishedP` should be resolved. The condition for + // this is if a short frame has been received and 1 extra frame has been + // received. This allows for the remote to close the connection. + if (!this.secured && this.shortReceived && !this.conn.isDraining()) { if (this.count >= 1) { this.secured = true; this.resolveSecureEstablishedP(); - // This.dispatchEvent(new events.QUICConnectionRemoteSecureEvent()); TODO } this.count += 1; } @@ -723,84 +707,24 @@ class QUICConnection extends EventTarget { this.resolveEstablishedP(); } - // We also need to know whether this is our first short frame - // After we are already established - // This may not be robust - // Cause technically - // What if we were "concatenated" packet - // Then it could be a problem right? - if (this.conn.isClosed()) { this.resolveClosedP(); return; } if (this.conn.isInEarlyData() || this.conn.isEstablished()) { - const readIds: Array = []; - for (const streamId of this.conn.readable() as Iterable) { - let quicStream = this.streamMap.get(streamId); - if (quicStream == null) { - // The creation will set itself to the stream map - quicStream = await QUICStream.createQUICStream({ - streamId, - connection: this, - codeToReason: this.codeToReason, - reasonToCode: this.reasonToCode, - // MaxReadableStreamBytes: this.maxReadableStreamBytes, - // maxWritableStreamBytes: this.maxWritableStreamBytes, - logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), - }); - this.dispatchEvent( - new events.QUICConnectionStreamEvent({ detail: quicStream }), - ); - } - readIds.push(quicStream.streamId); - quicStream.read(); - // QuicStream.dispatchEvent(new events.QUICStreamReadablaeEvent()); // TODO: remove? - } - if (readIds.length > 0) { - this.logger.info(`processed reads for ${readIds}`); - } - const writeIds: Array = []; - for (const streamId of this.conn.writable() as Iterable) { - let quicStream = this.streamMap.get(streamId); - if (quicStream == null) { - // The creation will set itself to the stream map - quicStream = await QUICStream.createQUICStream({ - streamId, - connection: this, - codeToReason: this.codeToReason, - reasonToCode: this.reasonToCode, - // MaxReadableStreamBytes: this.maxReadableStreamBytes, - logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), - }); - this.dispatchEvent( - new events.QUICConnectionStreamEvent({ detail: quicStream }), - ); - } - // QuicStream.dispatchEvent(new events.QUICStreamWritableEvent()); // TODO: remove? - writeIds.push(quicStream.streamId); - quicStream.write(); - } - if (writeIds.length > 0) { - this.logger.info(`processed writes for ${writeIds}`); - } + await this.processStreams(); } } finally { - // This.garbageCollectStreams('recv'); // FIXME: this was removed? How is this handled now? this.logger.debug('RECV FINALLY'); - // Set the timeout - this.setConnTimeOutTimer(); // FIXME: Might not be needed here, Only need it after calling send - // If this call wasn't executed in the midst of a destroy - // and yet the connection is closed or is draining, then - // we need to destroy this connection if ( this[status] !== 'destroying' && (this.conn.isClosed() || this.conn.isDraining()) ) { - this.logger.debug('CALLING DESTROY 2'); - // Destroy in the background, we still need to process packets - void this.stop({}, mon).catch(() => {}); + this.logger.debug('calling stop due to closed or draining'); + // Destroy in the background, we still need to process packets. + // Draining means no more packets are sent, so streams must be force closed. + void this.stop({ force: true }, mon).catch(() => {}); } } } @@ -877,8 +801,10 @@ class QUICConnection extends EventTarget { utils.certificateDERToPEM(c), ); try { + // Running verify callback if available if (this.verifyCallback != null) this.verifyCallback(peerCertsPem); this.logger.debug('TLS verification succeeded'); + // Generate ack frame to satisfy the short + 1 condition of secure establishment this.conn.sendAckEliciting(); } catch (e) { // Force the connection to end. @@ -895,124 +821,120 @@ class QUICConnection extends EventTarget { } } } - - // Check the header type - if (!this.shortSent) { - const header = quiche.Header.fromSlice( - sendBuffer, - quiche.MAX_CONN_ID_LEN, - ); - // If short frame - if (header.ty === 5) { - // Short was sent, locally secured - this.shortSent = true; - } - } } catch (e) { - // If called `stop` due to an error here - // we MUST not call `this.send` again - // in fact, we do a hard-stop - // There's no need to even have a timeout at all - // Remember this exception COULD be due to `e` - // It could be due to `localError` or `remoteError` - // All of this is possible - // Generally at least one of them is the reason - - // the error has to be one or the other - + // An error here means a hard failure in sending, we must force clean up + // since any further communication is expected to fail. + this.logger.debug(`Calling stop due to sending error [${e.message}]`); + const code = await this.reasonToCode('send', e); await this.stop( { - applicationError: true, - errorCode: 0, // TODO: actual code? use code mapping? + applicationError: false, + errorCode: code, errorMessage: e.message, + force: true, }, mon, ); - // We need to finish without any exceptions return; } if (this.conn.isClosed()) { - // But if it is closed with no error - // Then we just have to proceed! - // Plus if we are called here + // Handle stream clean up if closed this.resolveClosedP(); await this.stop( this.conn.localError() ?? this.conn.peerError() ?? {}, mon, ); - } else { - // In all other cases, reset the conn timer - this.setConnTimeOutTimer(); + } + this.setConnTimeOutTimer(); + } + + /** + * Keeps stream processing logic all in one place. + */ + protected async processStreams() { + for (const streamId of this.conn.readable() as Iterable) { + let quicStream = this.streamMap.get(streamId); + if (quicStream == null) { + // The creation will set itself to the stream map + quicStream = await QUICStream.createQUICStream({ + streamId, + connection: this, + codeToReason: this.codeToReason, + reasonToCode: this.reasonToCode, + logger: this.logger.getChild(`${QUICStream.name} ${streamId}`), + }); + quicStream.addEventListener( + 'streamDestroy', + this.handleQUICStreamDestroyEvent, + { once: true }, + ); + this.dispatchEvent( + new events.QUICConnectionStreamEvent({ detail: quicStream }), + ); + } + quicStream.read(); + } + for (const streamId of this.conn.writable() as Iterable) { + const quicStream = this.streamMap.get(streamId); + if (quicStream == null) { + // This is a dead case, there are only two ways streams are created. + // The QUICStream will always exist before processing it's writable. + // 1. First time it is seen in the readable iterator + // 2. created using `streamNew()` + never(); + } + quicStream.write(); } } protected setConnTimeOutTimer(): void { + const logger = this.logger.getChild('timer'); const connTimeOutHandler = async () => { - // This can only be called when the timeout has occurred - // This transitions the connection state - this.logger.debug('CALLING ON TIMEOUT'); + // This can only be called when the timeout has occurred. + // This transitions the connection state. + // `conn.timeout()` is time aware, so calling `conn.onTimeout` will only + // trigger state transitions after the time has passed. + logger.debug('CALLING ON TIMEOUT'); this.conn.onTimeout(); - // At this point... - // we check the conditions on the connection - // This way we can RESOLVE things like - // conn closed or established or other things - // or if we are draining - // if we have timed out... etc - // All state changes need to result in reaction - // Is established is not required - // But we can have a bunch of things that check and react accordingly - // So if it is timed out, it gets closed too - // But if it is is timed out due to idle we raise an error - - // At the same time, we may in fact be closed too + // Connection may have closed after timing out if (this.conn.isClosed()) { - // If it was still starting waiting for the secure event, - // we need to reject that promise. + // If it was still starting waiting for the secure event, we need to + // reject the `secureEstablishedP` promise. if (this[status] === 'starting') { this.rejectSecureEstablishedP( new errors.ErrorQUICConnectionInternal('Connection has closed!'), ); } - // We actually finally closed here - // Actually the question is that this could be an error - // The act of closing is an error? - // That's confusing + logger.debug('resolving closedP'); + // We resolve closing here, stop checks if the connection has timed out + // and handles it. this.resolveClosedP(); - // If we are not stopping nor are we stopped - // And we are not running, call await this stop - - // We need to trigger this as well by calling stop - // What happens if we are starting too? + // If we are still running and not stopping then we need to stop if (this[running] && this[status] !== 'stopping') { - // If we are already stopping, stop multiple times is idempotent - // Wait if we call stop multiple times - // Actually we may already be stopping - // But also that if the status is starting - // But also if we are starting - // Resolve the closeP - // is technically an error! const mon = new Monitor(this.lockbox, RWLockWriter); - await this.stop( - this.conn.localError() ?? this.conn.peerError() ?? {}, - mon, - ); + // Background stopping, we don't want to block the timer resolving + void this.stop({ force: true }, mon); } - - // Finish + logger.debug('CLEANING UP TIMER'); return; } const mon = new Monitor(this.lockbox, RWLockWriter); - await this.send(mon); + // There may be data to send after timing out + void this.send(mon); // Note that a `0` timeout is still a valid timeout const timeout = this.conn.timeout(); - // If this is `null`, then technically there's nothing to do - if (timeout == null) return; + // If this is `null`, then quiche is requesting the timer to be cleaned up + if (timeout == null) { + logger.debug('CLEANING UP TIMER'); + return; + } // Allow an extra 1ms for the delay to fully complete, so we can avoid a repeated 0ms delay + logger.debug(`Recreating timer with ${timeout + 1} delay`); this.connTimeOutTimer = new Timer({ delay: timeout + 1, handler: connTimeOutHandler, @@ -1020,16 +942,27 @@ class QUICConnection extends EventTarget { }; // Note that a `0` timeout is still a valid timeout const timeout = this.conn.timeout(); - // If this is `null` there's nothing to do - if (timeout == null) return; + // If this is `null`, then quiche is requesting the timer to be cleaned up + if (timeout == null) { + // Clean up timer if it is running + if ( + this.connTimeOutTimer != null && + this.connTimeOutTimer.status === null + ) { + logger.debug('CLEANING UP TIMER'); + this.connTimeOutTimer.cancel(timerCleanupReasonSymbol); + } + return; + } // If there was an existing timer, we cancel it and set a new one if ( this.connTimeOutTimer != null && this.connTimeOutTimer.status === null ) { - this.connTimeOutTimer.reset(timeout); + logger.debug(`resetting timer with ${timeout + 1} delay`); + this.connTimeOutTimer.reset(timeout + 1); } else { - this.logger.debug(`timeout created with delay ${timeout}`); + logger.debug(`timeout created with delay ${timeout}`); this.connTimeOutTimer = new Timer({ delay: timeout + 1, handler: connTimeOutHandler, @@ -1042,7 +975,7 @@ class QUICConnection extends EventTarget { * Make sure to set the interval to be less than then the `maxIdleTime` unless * if the `maxIdleTime` is `0`. * If the `maxIdleTime` is `0`, then this is not needed to keep the connection - * open. However it can still be useful to maintain liveness for NAT purposes. + * open. However, it can still be useful to maintain liveliness for NAT purposes. */ protected startKeepAliveIntervalTimer(ms: number): void { const keepAliveHandler = async () => { @@ -1067,7 +1000,7 @@ class QUICConnection extends EventTarget { * Stops the keep alive interval timer */ protected stopKeepAliveIntervalTimer(): void { - this.keepAliveIntervalTimer?.cancel(); + this.keepAliveIntervalTimer?.cancel(timerCleanupReasonSymbol); } /** @@ -1077,12 +1010,7 @@ class QUICConnection extends EventTarget { */ @ready(new errors.ErrorQUICConnectionNotRunning()) public async streamNew(streamType: 'bidi' = 'bidi'): Promise { - // You wouldn't want AsyncMonitor here - // The problem is that we want re-entrant contexts - - // Technically you can do concurrent bidi and uni style streams - // but no support for uni streams yet - // So we don't bother with it + // Using a lock on stream ID to prevent racing updates return await this.streamIdLock.withF(async () => { let streamId: StreamId; if (this.type === 'client' && streamType === 'bidi') { @@ -1091,30 +1019,18 @@ class QUICConnection extends EventTarget { streamId = this.streamIdServerBidi; } - // If you call this again - // you will get another stream ID - // but the problem is that - // if you call it multiple times concurrently - // you'll have an issue - // this can only be called one at a time - // This is not allowed to be concurrent - // You cannot open many streams all concurrently - // since stream creations are serialised - - // If we are in draining state - // we cannot call this anymore - // Hre ewe send the stream id - // with stream capacity will fail - // We send a 0-length buffer first const quicStream = await QUICStream.createQUICStream({ streamId: streamId!, connection: this, codeToReason: this.codeToReason, reasonToCode: this.reasonToCode, - // MaxReadableStreamBytes: this.maxReadableStreamBytes, - // maxWritableStreamBytes: this.maxWritableStreamBytes, logger: this.logger.getChild(`${QUICStream.name} ${streamId!}`), }); + quicStream.addEventListener( + 'streamDestroy', + this.handleQUICStreamDestroyEvent, + { once: true }, + ); // Ok the stream is opened and working if (this.type === 'client' && streamType === 'bidi') { this.streamIdClientBidi = (this.streamIdClientBidi + 4) as StreamId; @@ -1124,128 +1040,6 @@ class QUICConnection extends EventTarget { return quicStream; }); } - - // /** - // * Used to update or disable the keep alive interval. - // * Calling this will reset the delay before the next keep alive. - // */ - // @ready(new errors.ErrorQUICConnectionNotRunning()) - // public setKeepAlive(intervalDelay?: number) { - // // Clearing timeout prior to update - // if (this.keepAliveInterval != null) { - // clearTimeout(this.keepAliveInterval); - // delete this.keepAliveInterval; - // } - // // Setting up keep alive interval - // if (intervalDelay != null) { - // this.keepAliveInterval = setInterval(async () => { - // // Trigger an ping frame and send - // this.conn.sendAckEliciting(); - // await this.send(); - // }, intervalDelay); - // } - // } - // - // // Timeout handling, these methods handle time keeping for quiche. - // // Quiche will request an amount of time, We then call `onTimeout()` after that time has passed. - // protected deadline: number = 0; - // protected onTimeout = async () => { - // this.logger.warn('ON TIMEOUT CALLED ' + new Date()); - // this.logger.debug('timeout on timeout'); - // // Clearing timeout - // clearTimeout(this.timer); - // delete this.timer; - // this.deadline = Infinity; - // // Doing timeout actions - // // console.time('INTERNAL ON TIMEOUT'); - // this.conn.onTimeout(); - // // console.timeEnd('INTERNAL ON TIMEOUT'); - // this.logger.warn('BEFORE CALLING SEND' + new Date()); - // if (this[destroyed] === false) await this.send(); - // this.logger.warn('AFTER CALLING SEND ' + new Date()); - // if ( - // this[status] !== 'destroying' && - // (this.conn.isClosed() || this.conn.isDraining()) - // ) { - // this.logger.debug('CALLING DESTROY 3'); - // // Destroy in the background, we still need to process packets - // void this.destroy().catch(() => {}); - // } - // this.logger.warn('BEFORE CHECK TIMEOUT' + new Date()); - // this.checkTimeout(); - // this.logger.warn('AFTER CHECK TIMEOUT' + new Date()); - // }; - - /** - * Checks the timeout event, should be called whenever the following events happen. - * 1. `send()` is called - * 2. `recv()` is called - * 3. timer times out. - * - * This needs to do 3 things. - * 1. Create a timer if none exists - * 2. Update the timer if `conn.timeout()` is less than current timeout. - * 3. clean up timer if `conn.timeout()` is null. - */ - // protected checkTimeout = () => { - // this.logger.debug('timeout checking timeout'); - // // During construction, this ends up being null - // const time = this.conn.timeout(); - // this.logger.error(`THE TIME (${this.times}): ` + time + ' ' + new Date()); - // this.times++; - // - // if (time == null) { - // // Clear timeout - // if (this.timer != null) this.logger.debug('timeout clearing timeout'); - // clearTimeout(this.timer); - // delete this.timer; - // this.deadline = Infinity; - // } else { - // const newDeadline = Date.now() + time; - // if (this.timer != null) { - // if (time === 0) { - // this.logger.debug('timeout triggering instant timeout'); - // // Skip timer and call onTimeout - // setImmediate(this.onTimeout); - // } else if (newDeadline < this.deadline) { - // this.logger.debug(`timeout updating timer with ${time} delay`); - // clearTimeout(this.timer); - // delete this.timer; - // this.deadline = newDeadline; - // - // this.logger.warn('BEFORE SET TIMEOUT 1: ' + time); - // - // this.timer = setTimeout(this.onTimeout, time); - // } - // } else { - // if (time === 0) { - // this.logger.debug('timeout triggering instant timeout'); - // // Skip timer and call onTimeout - // setImmediate(this.onTimeout); - // return; - // } - // this.logger.debug(`timeout creating timer with ${time} delay`); - // this.deadline = newDeadline; - // - // this.logger.warn('BEFORE SET TIMEOUT 2: ' + time); - // - // this.timer = setTimeout(this.onTimeout, time); - // } - // } - // }; - - // protected garbageCollectStreams(where: string) { - // const nums: Array = []; - // // Only check if packets were sent - // for (const [streamId, quicStream] of this.streamMap) { - // // Stream sending can finish after a packet is sent - // nums.push(streamId); - // quicStream.read(); - // } - // if (nums.length > 0) { - // this.logger.info(`checking read finally ${where} for ${nums}`); - // } - // } } export default QUICConnection; diff --git a/src/QUICServer.ts b/src/QUICServer.ts index ff9a62f6..a7cc9f9d 100644 --- a/src/QUICServer.ts +++ b/src/QUICServer.ts @@ -22,10 +22,11 @@ import { quiche } from './native'; import * as utils from './utils'; import * as errors from './errors'; import QUICSocket from './QUICSocket'; +import { never } from './utils'; /** - * You must provide a error handler `addEventListener('error')`. - * Otherwise errors will just be ignored. + * You must provide an error handler `addEventListener('error')`. + * Otherwise, errors will just be ignored. * * Events: * - serverStop @@ -54,22 +55,52 @@ class QUICServer extends EventTarget { protected codeToReason: StreamCodeToReason | undefined; protected verifyCallback: VerifyCallback | undefined; protected connectionMap: QUICConnectionMap; + // Used to track address string for logging ONLY + protected address: string; protected handleQUICSocketEvents = (e: events.QUICSocketEvent) => { - const event = new Event('asd'); - this.dispatchEvent(event); - this.dispatchEvent(event); if (e instanceof events.QUICSocketErrorEvent) { this.dispatchEvent( new events.QUICServerErrorEvent({ detail: e.detail, }), ); + // Trigger clean up + this.logger.debug('calling stop due to socket error'); + void this.stop({ force: true }); + } else if (e instanceof events.QUICSocketStopEvent) { + this.dispatchEvent(new events.QUICSocketStopEvent()); + // Trigger clean up + this.logger.debug('calling stop due to socket stop'); + void this.stop({ force: true }); + } else if (e instanceof events.QUICSocketStartEvent) { + this.dispatchEvent(new events.QUICSocketStartEvent()); + } else { + // Should never happen, all cases should be covered + never(); } }; - protected handleQUICConnectionEvents = (e: events.QUICConnectionEvent) => { - this.dispatchEvent(e); + protected handleQUICConnectionEvents = ( + event: events.QUICConnectionEvent, + ) => { + if (event instanceof events.QUICConnectionErrorEvent) { + this.dispatchEvent( + new events.QUICConnectionErrorEvent({ + detail: event.detail, + }), + ); + } else if (event instanceof events.QUICConnectionStopEvent) { + this.dispatchEvent(new events.QUICConnectionStopEvent()); + } else if (event instanceof events.QUICConnectionStreamEvent) { + this.dispatchEvent( + new events.QUICConnectionStreamEvent({ detail: event.detail }), + ); + } else if (event instanceof events.QUICStreamDestroyEvent) { + this.dispatchEvent(new events.QUICStreamDestroyEvent()); + } else { + utils.never(); + } }; public constructor({ @@ -152,6 +183,7 @@ class QUICServer extends EventTarget { let address: string; if (!this.isSocketShared) { address = utils.buildAddress(host, port); + this.address = address; this.logger.info(`Start ${this.constructor.name} on ${address}`); await this.socket.start({ host, port, reuseAddr }); address = utils.buildAddress(this.socket.host, this.socket.port); @@ -179,8 +211,7 @@ class QUICServer extends EventTarget { }: { force?: boolean; } = {}) { - // Console.time('destroy conn'); - const address = utils.buildAddress(this.socket.host, this.socket.port); + const address = this.address; this.logger.info(`Stop ${this.constructor.name} on ${address}`); const destroyProms: Array> = []; for (const connection of this.connectionMap.serverConnections.values()) { @@ -188,13 +219,13 @@ class QUICServer extends EventTarget { connection.stop({ applicationError: true, errorMessage: 'cleaning up connections', - errorCode: 42, force, }), - ); // TODO: fill in with proper details + ); } + this.logger.debug('Awaiting connections to destroy'); await Promise.all(destroyProms); - // Console.timeEnd('destroy conn'); + this.logger.debug('All connections destroyed'); this.socket.deregisterServer(this); if (!this.isSocketShared) { // If the socket is not shared, then it can be stopped @@ -340,23 +371,55 @@ class QUICServer extends EventTarget { ), }); try { - await connectionProm; // TODO: pass ctx + await connectionProm; } catch (e) { // Ignoring any errors here as a failure to connect - // FIXME: should we emit a connection error here? + this.dispatchEvent( + new events.QUICConnectionErrorEvent({ + detail: new errors.ErrorQUICServerConnectionFailed(undefined, { + cause: e, + }), + }), + ); return; } const connection = await connectionProm; + // Handling connection events + connection.addEventListener( + 'connectionError', + this.handleQUICConnectionEvents, + ); + connection.addEventListener( + 'connectionStream', + this.handleQUICConnectionEvents, + ); + connection.addEventListener( + 'streamDestroy', + this.handleQUICConnectionEvents, + ); + connection.addEventListener( + 'connectionStop', + (event) => { + connection.removeEventListener( + 'connectionError', + this.handleQUICConnectionEvents, + ); + connection.removeEventListener( + 'connectionStream', + this.handleQUICConnectionEvents, + ); + connection.removeEventListener( + 'streamDestroy', + this.handleQUICConnectionEvents, + ); + this.handleQUICConnectionEvents(event); + }, + { once: true }, + ); this.dispatchEvent( new events.QUICServerConnectionEvent({ detail: connection }), ); - // A new conn ID means a new connection - // the old connection gets removed - // so one has to be aware of this - // Either that, or there is a seamless migration to a new connection ID - // In which case we need to manage it somehow - return connection; } diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index 3ea9909c..8b663127 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -85,8 +85,6 @@ class QUICSocket extends EventTarget { return; } // At this point, the connection may not yet be started - // FIXME: How can we be awaiting connection secured event WHILE - // processing packets for it? const connection_ = await this.server.connectionNew( remoteInfo_, header, diff --git a/src/QUICStream.ts b/src/QUICStream.ts index 13df2ab4..4c6e1c3e 100644 --- a/src/QUICStream.ts +++ b/src/QUICStream.ts @@ -27,6 +27,7 @@ import { quiche } from './native'; import * as events from './events'; import * as utils from './utils'; import * as errors from './errors'; +import { never } from './utils'; /** * Events: @@ -58,6 +59,7 @@ class QUICStream protected _recvClosed: boolean = false; protected resolveReadableP?: () => void; protected resolveWritableP?: () => void; + protected destroyProm = utils.promise(); /** * For `reasonToCode`, return 0 means "unknown reason" @@ -83,11 +85,11 @@ class QUICStream }): Promise { logger.info(`Create ${this.name}`); // 'send' a 0-len message to initialize stream state in Quiche. No 0-len data is actually sent so this does not - // create remote state. + // create Peer state. try { connection.conn.streamSend(streamId, new Uint8Array(0), false); } catch { - // Ignore errors, we only want to initialize local state here. + // FIXME: If there is an error here then stream will not create? Maybe we should abort? } const stream = new this({ streamId, @@ -125,38 +127,34 @@ class QUICStream this.readable = new ReadableStream( { - // Type: 'bytes', start: (controller) => { this.readableController = controller; }, pull: async (controller) => { - this.logger.warn('attempting data pull'); // If nothing to read then we wait if (!this.conn.streamReadable(this.streamId)) { const readProm = utils.promise(); this.resolveReadableP = readProm.resolveP; - this.logger.warn('waiting for readable'); + this.logger.debug('readable waiting for more data'); await readProm.p; + if (!this.conn.streamReadable(this.streamId)) { + // If there is nothing to read then we are tying up loose ends, + // do nothing and return. I don't think this will even happen though. + return; + } + this.logger.debug('readable resuming'); } const buf = Buffer.alloc(1024); let recvLength: number, fin: boolean; - try { - [recvLength, fin] = this.conn.streamRecv(this.streamId, buf); - } catch (e) { - if (e.message === 'Done') { - // When it is reported to be `Done`, it just means that there is no data to read - // it does not mean that the stream is closed or finished - // In such a case, we just ignore and continue - // However after the stream is closed, then it would continue to return `Done` - // This can only occur in 2 ways, either via the `fin` - // or through an exception here where the stream reports an error - // Since we don't call this method unless it is readable - // This should never be reported... (this branch should be dead code) - return; - } else { + // Read messages until buffer is empty + while (true) { + try { + [recvLength, fin] = this.conn.streamRecv(this.streamId, buf); + } catch (e) { this.logger.debug(`Stream recv reported: error ${e.message}`); - if (!this._recvClosed) { + // Done means there is no more data to read + if (!this._recvClosed && e.message !== 'Done') { const reason = (await this.processSendStreamError(e, 'recv')) ?? e; // If it is `StreamReset(u64)` error, then the peer has closed @@ -166,24 +164,22 @@ class QUICStream controller.error(reason); await this.closeRecv(true, reason); } - return; + break; } - } - this.logger.debug(`stream read ${recvLength} bytes with fin(${fin})`); - // If fin is true, then that means, the stream is CLOSED - if (!fin) { - // Send the data normally - if (!this._recvClosed) { + this.logger.debug( + `stream read ${recvLength} bytes with fin(${fin})`, + ); + // Check and drop if we're already closed or message is 0-length message + if (!this._recvClosed && recvLength > 0) { this.readableController.enqueue(buf.subarray(0, recvLength)); } - } else { - // Strip the end message, removing the null byte - if (!this._recvClosed && recvLength > 1) { - this.readableController.enqueue(buf.subarray(0, recvLength - 1)); + // If fin is true, then that means, the stream is CLOSED + if (fin) { + await this.closeRecv(); + controller.close(); + // Return out of the loop + break; } - await this.closeRecv(); - controller.close(); - return; } }, cancel: async (reason) => { @@ -192,6 +188,7 @@ class QUICStream }, }, new CountQueuingStrategy({ + // Allow 1 buffered message, so we can know when data is desired, and we can know when to un-pause. highWaterMark: 1, }), ); @@ -206,28 +203,20 @@ class QUICStream await this.connection.send(); }, close: async () => { - // This gracefully closes, by sending a message at the end - // If there wasn't an error, we will send an empty frame - // with the `fin` set to true - // If this itself results in an error, we can continue - // But continue to do the below + // Gracefully ends the stream with a 0-length fin frame this.logger.debug('sending fin frame'); - // This.sendFinishedProm.resolveP(); - await this.streamSend(Buffer.from([0]), true); + await this.streamSend(new Uint8Array(0), true); // Close without error await this.closeSend(); }, abort: async (reason?: any) => { - // Abort can be called even if there are writes are queued up - // The chunks are meant to be thrown away - // We could tell it to shut down - // This sends a `RESET_STREAM` frame, this abruptly terminates the writing part of a stream - // The receiver can discard any data it already received on that stream - // We don't have "unidirectional" streams so that's not important... + // Forces the stream to immediately close with an error. Will trigger a `RESET_STREAM` frame to be sent to + // the peer. Any buffered data is discarded. await this.closeSend(true, reason); }, }, { + // Allow 1 buffered 'message', Buffering is handled via quiche highWaterMark: 1, }, ); @@ -241,12 +230,22 @@ class QUICStream return this._recvClosed; } + public get destroyedP() { + return this.destroyProm.p; + } + /** * Connection information including hosts, ports and cert data. */ + @ready(new errors.ErrorQUICStreamDestroyed()) public get remoteInfo(): ConnectionMetadata { - throw Error('TMP IMP'); - // Return this.connection.remoteInfo; + return { + localHost: this.connection.localHost, + localPort: this.connection.localPort, + remoteCertificates: this.connection.getRemoteCertsChain(), + remoteHost: this.connection.remoteHost, + remotePort: this.connection.remotePort, + }; } /** @@ -254,8 +253,7 @@ class QUICStream * This strictly exists to work with agnostic RPC stream interface. */ public get meta(): ConnectionMetadata { - throw Error('TMP IMP'); - // Return this.connection.remoteInfo; + return this.remoteInfo; } /** @@ -268,26 +266,19 @@ class QUICStream * directions have closed. Or when force closing the connection which does not * require waiting. */ - public async destroy({ force = false }: { force?: boolean } = {}) { + public async destroy() { this.logger.info(`Destroy ${this.constructor.name}`); - if (!this._recvClosed && force) { - const e = new errors.ErrorQUICStreamClose(); - this.readableController.error(e); - await this.closeRecv(true, e); - } - if (!this._sendClosed && force) { - const e = new errors.ErrorQUICStreamClose(); - this.writableController.error(e); - await this.closeSend(true, e); - } - await this.connection.send(); + // Force close any open streams + this.cancel(new errors.ErrorQUICStreamClose()); + // Removing stream from the connection's stream map this.streamMap.delete(this.streamId); this.dispatchEvent(new events.QUICStreamDestroyEvent()); this.logger.info(`Destroyed ${this.constructor.name}`); } /** - * Used to cancel the streams. This function is synchronous but triggers some asynchronous events. + * Used to cancel the streams. This function is synchronous and will immediately close the stream and not await any + * response. */ public cancel(reason?: any): void { reason = reason ?? new errors.ErrorQUICStreamCancel(); @@ -307,15 +298,50 @@ class QUICStream */ @ready(new errors.ErrorQUICStreamDestroyed(), false, ['destroying']) public read(): void { - // If we're readable and the readable stream is still waiting, then we need to push some data. - // Or if the stream has finished we need to read and clean up. - this.logger.warn(`desired size ${this.readableController.desiredSize}`); + // If we're readable then we need to un-pause the readable stream. + // We also need to check for an early end condition here. + this.logger.debug(`desired size ${this.readableController.desiredSize}`); + if (this.conn.streamFinished(this.streamId)) { + this.logger.debug( + 'stream is finished and readable, processing end condition', + ); + // If we're finished and read was called then we need to read out the last message + // to check if it's a fin frame or an error. + // This duplicates some of the pull logic for processing an error or a fin frame. + // No actual data is expected in this case. + const buf = Buffer.alloc(1024); + let fin: boolean; + try { + [, fin] = this.conn.streamRecv(this.streamId, buf); + if (fin) { + // Closing the readable stream + void this.closeRecv(); + this.readableController.close(); + } + } catch (e) { + if (e.message === 'Done') { + never(); + } else { + this.logger.debug(`Stream recv reported: error ${e.message}`); + if (!this._recvClosed) { + // Close stream in background + void (async () => { + const reason = + (await this.processSendStreamError(e, 'recv')) ?? e; + this.readableController.error(reason); + await this.closeRecv(true, reason); + })(); + } + } + } + // Clean up the readable block so any waiting read can finish + if (this.resolveReadableP != null) this.resolveReadableP(); + } + // Check if the readable is waiting for data and resolve the block if ( - (this.readableController.desiredSize != null && - this.readableController.desiredSize > 0) || - this.conn.streamFinished(this.streamId) + this.readableController.desiredSize != null && + this.readableController.desiredSize > 0 ) { - // We resolve the read block if (this.resolveReadableP != null) this.resolveReadableP(); } } @@ -326,68 +352,45 @@ class QUICStream */ @ready(new errors.ErrorQUICStreamDestroyed(), false, ['destroying']) public write(): void { - // Checking if the writable had an error try { + // Checking if the writable had an error this.conn.streamWritable(this.streamId, 0); } catch (e) { // If it threw an error, then the stream was closed with an error // We need to attempt a write to trigger state change and remove stream from writable iterator void this.streamSend(Buffer.from('dummy data'), true).catch(() => {}); } + // Resolve the write blocking promise if (this.resolveWritableP != null) { this.resolveWritableP(); } } protected async streamSend(chunk: Uint8Array, fin = false): Promise { - // This means that the number of written bytes returned can be lower - // than the length of the input buffer when the stream doesn't have - // enough capacity for the operation to complete. The application - // should retry the operation once the stream is reported as writable again. - let sentLength: number; + // Check if we have capacity to send. Doing so will signal to quiche how many bytes are waiting and the stream will + // not become writable until there is room. So we can wait for the space before sending. try { - sentLength = this.conn.streamSend(this.streamId, chunk, fin); + // Checking if stream has capacity and wait for room. + if (!this.conn.streamWritable(this.streamId, chunk.byteLength)) { + this.logger.debug( + `stream does not have capacity for ${chunk.byteLength} bytes, waiting for capacity`, + ); + const { p: writableP, resolveP: resolveWritableP } = utils.promise(); + this.resolveWritableP = resolveWritableP; + await writableP; + } + + const sentLength = this.conn.streamSend(this.streamId, chunk, fin); + // Since we are checking beforehand, we never not send the whole message + if (sentLength < chunk.byteLength) never(); this.logger.debug(`stream wrote ${sentLength} bytes with fin(${fin})`); } catch (e) { - // If the Done is returned - // then no data was sent - // because the stream has no capacity - if (e.message === 'Done') { - // If the chunk size itself is 0, - // it is still possible to have no capacity - // to send a 0-length buffer. - // To indicate this we set the sentLength to -1. - // This ensures that we are always blocked below. - sentLength = -1; - } else { - // Signal sending has ended - // We may receive a `StreamStopped(u64)` exception - // meaning the peer has signalled for us to stop writing - // If this occurs, we need to go back to the writable stream - // and indicate that there was an error now - // Actually it's sufficient to simply throw an exception I think - // That would essentially do it - const reason = (await this.processSendStreamError(e, 'send')) ?? e; - // We have to close the send side (but the stream is already closed) - await this.closeSend(true, reason); - // Throws the exception back to the writer - throw reason; - } - } - if (sentLength < chunk.length) { - const { p: writableP, resolveP: resolveWritableP } = utils.promise(); - this.resolveWritableP = resolveWritableP; - this.logger.debug( - `stream wrote only ${sentLength}/${chunk.byteLength} bytes, waiting for capacity`, - ); - await writableP; - // If the `sentLength` is -1, then it will be raised to `0` - const remainingMessage = chunk.subarray(Math.max(sentLength, 0)); - await this.streamSend(remainingMessage, fin); - this.logger.debug( - `stream wrote remaining ${remainingMessage.byteLength} bytes with fin(${fin})`, - ); - return; + // We can fail with an error. Likely a `StreamStopped(u64)` exception indicating the stream has + // failed in some way. We need to process the error and propagate it to the web-stream. + const reason = (await this.processSendStreamError(e, 'send')) ?? e; + await this.closeSend(true, reason); + // Throws the exception back to the writer + throw reason; } } @@ -406,11 +409,11 @@ class QUICStream this.logger.debug(`Close Recv`); // Indicate that the receiving side is closed this._recvClosed = true; - const code = isError ? await this.reasonToCode('send', reason) : 0; - // This will send a `STOP_SENDING` frame with the code - // When the other peer sends, they will get a `StreamStopped(u64)` exception if (isError) { try { + const code = isError ? await this.reasonToCode('send', reason) : 0; + // This will send a `STOP_SENDING` frame with the code + // When the other peer sends, they will get a `StreamStopped(u64)` exception this.conn.streamShutdown(this.streamId, quiche.Shutdown.Read, code); } catch (e) { // Ignore if already shutdown @@ -418,11 +421,13 @@ class QUICStream } this.readableController.error(reason); } - await this.connection.send(); - if (this[status] !== 'destroying' && this._recvClosed && this._sendClosed) { + // Background the send to avoid deadlock + void this.connection.send(); + if (this._recvClosed && this._sendClosed) { // Only destroy if we are not already destroying // and that both recv and send is closed - void this.destroy(); + this.destroyProm.resolveP(); + if (this[status] !== 'destroying') void this.destroy(); } this.logger.debug(`Closed Recv`); } @@ -445,8 +450,6 @@ class QUICStream this._sendClosed = true; if (isError) { try { - // If the QUIC stream is already closed - // there's nothing to do on the QUIC stream const code = await this.reasonToCode('send', reason); // This will send a `RESET_STREAM` frame with the code // When the other peer receives, they will get a `StreamReset(u64)` exception @@ -457,11 +460,13 @@ class QUICStream } this.writableController.error(reason); } - await this.connection.send(); - if (this[status] !== 'destroying' && this._recvClosed && this._sendClosed) { + // Background the send to avoid deadlock + void this.connection.send(); + if (this._recvClosed && this._sendClosed) { // Only destroy if we are not already destroying // and that both recv and send is closed - void this.destroy(); + this.destroyProm.resolveP(); + if (this[status] !== 'destroying') void this.destroy(); } this.logger.debug(`Closed Send`); } diff --git a/src/bin/server.ts b/src/bin/server.ts index dfbcd9ae..a23bafdf 100644 --- a/src/bin/server.ts +++ b/src/bin/server.ts @@ -4,11 +4,14 @@ import type { Host, Port } from '../types'; import type * as events from '../events'; import process from 'process'; import { webcrypto } from 'crypto'; +import fs from 'fs'; import Logger from '@matrixai/logger'; import QUICServer from '../QUICServer'; -async function main(_argv = process.argv): Promise { - _argv = _argv.slice(2); // Removing prepended file paths +async function main(argv = process.argv): Promise { + argv = argv.slice(2); // Removing prepended file paths + const privateKeyPem = await fs.promises.readFile(argv[0]); + const certPem = await fs.promises.readFile(argv[1]); const cryptoKey = await webcrypto.subtle.generateKey( { @@ -51,10 +54,8 @@ async function main(_argv = process.argv): Promise { crypto, logger: logger.getChild(QUICServer.name), config: { - tlsConfig: { - privKeyFromPemFile: './tests/fixtures/certs/okp1.key', - certChainFromPemFile: './tests/fixtures/certs/okp1.crt', - }, + key: privateKeyPem, + cert: certPem, }, }); diff --git a/src/errors.ts b/src/errors.ts index 579ef510..f277c507 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -46,6 +46,10 @@ class ErrorQUICServerSocketNotRunning extends ErrorQUICServer { 'QUIC Server cannot start with an unstarted shared QUIC socket'; } +class ErrorQUICServerConnectionFailed extends ErrorQUICServer { + static description = 'QUIC server failed to create or accept a connection'; +} + class ErrorQUICClient extends ErrorQUIC { static description = 'QUIC Client error'; } @@ -124,11 +128,6 @@ class ErrorQUICStreamDestroyed extends ErrorQUICStream { static description = 'QUIC Stream is destroyed'; } -class ErrorQUICStreamLocked extends ErrorQUICStream { - static description = - 'QUIC Stream is locked and is not closed on readable or writable'; -} - class ErrorQUICStreamClose extends ErrorQUICStream { static description = 'QUIC Stream force close'; } @@ -137,10 +136,6 @@ class ErrorQUICStreamCancel extends ErrorQUICStream { static description = 'QUIC Stream was cancelled without a provided reason'; } -class ErrorQUICStreamUnexpectedClose extends ErrorQUICStream { - static description = 'QUIC Stream closed early with no reason given'; -} - class ErrorQUICUndefinedBehaviour extends ErrorQUIC { static description = 'This should never happen'; } @@ -157,6 +152,7 @@ export { ErrorQUICServer, ErrorQUICServerNotRunning, ErrorQUICServerSocketNotRunning, + ErrorQUICServerConnectionFailed, ErrorQUICClient, ErrorQUICClientCreateTimeOut, ErrorQUICClientDestroyed, @@ -170,9 +166,7 @@ export { ErrorQUICConnectionInvalidConfig, ErrorQUICStream, ErrorQUICStreamDestroyed, - ErrorQUICStreamLocked, ErrorQUICStreamClose, ErrorQUICStreamCancel, - ErrorQUICStreamUnexpectedClose, ErrorQUICUndefinedBehaviour, }; diff --git a/src/events.ts b/src/events.ts index 925d752f..04434ecd 100644 --- a/src/events.ts +++ b/src/events.ts @@ -107,12 +107,6 @@ class QUICConnectionStreamEvent extends QUICConnectionEvent { } } -class QUICConnectionStartEvent extends QUICConnectionEvent { - constructor(options?: EventInit) { - super('connectionStart', options); - } -} - class QUICConnectionStopEvent extends QUICConnectionEvent { constructor(options?: EventInit) { super('connectionStop', options); @@ -156,7 +150,6 @@ export { QUICServerErrorEvent, QUICConnectionEvent, QUICConnectionStreamEvent, - QUICConnectionStartEvent, QUICConnectionStopEvent, QUICConnectionErrorEvent, QUICStreamEvent, diff --git a/src/native/napi/config.rs b/src/native/napi/config.rs index 81dcc234..e167fab2 100644 --- a/src/native/napi/config.rs +++ b/src/native/napi/config.rs @@ -68,7 +68,7 @@ impl Config { }; ssl_ctx_builder.set_verify_callback(verify_value, move |pre_verify, _| { // Override any validation errors, this is needed so we can request certs but validate them - // manually. + // manually. It's essentially allowing insecure certificates if verify_allow_fail { true } else { diff --git a/src/types.ts b/src/types.ts index d6887b10..34595243 100644 --- a/src/types.ts +++ b/src/types.ts @@ -174,7 +174,7 @@ type QUICConfig = { verifyPeer: boolean; /** - * Will allow unsecure TLS certs, allowing for certs to be requested + * Will allow insecure TLS certs, allowing for certs to be requested * but the verification result is ignored. */ verifyAllowFail: boolean; diff --git a/tests/QUICClient.test.ts b/tests/QUICClient.test.ts index 51e41eb2..b0dc7597 100644 --- a/tests/QUICClient.test.ts +++ b/tests/QUICClient.test.ts @@ -715,8 +715,7 @@ describe(QUICClient.name, () => { await server.stop(); }); }); - // FIXME: These tests are failing pending the stream changes. - describe.skip('handles random packets', () => { + describe('handles random packets', () => { testProp( 'client handles random noise from server', [ diff --git a/tests/QUICStream.test.ts b/tests/QUICStream.test.ts index a6e66709..3b3ecf91 100644 --- a/tests/QUICStream.test.ts +++ b/tests/QUICStream.test.ts @@ -1,8 +1,7 @@ import type * as events from '@/events'; import type { Host, Port } from '@'; -import type QUICSocket from '@/QUICSocket'; import type { ClientCrypto, ServerCrypto } from '@'; -import { testProp, fc } from '@fast-check/jest'; +import type QUICSocket from '@/QUICSocket'; import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; import { destroyed } from '@matrixai/async-init'; import * as utils from '@/utils'; @@ -13,12 +12,13 @@ import * as testsUtils from './utils'; import { generateConfig } from './utils'; describe(QUICStream.name, () => { - const logger = new Logger(`${QUICStream.name} Test`, LogLevel.DEBUG, [ + const logger = new Logger(`${QUICStream.name} Test`, LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), ]); const defaultType = 'RSA'; + const localhost = '127.0.0.1' as Host; // This has to be setup asynchronously due to key generation const serverCrypto: ServerCrypto = { sign: testsUtils.signHMAC, @@ -66,12 +66,12 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '::' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -105,8 +105,8 @@ describe(QUICStream.name, () => { }), ]); expect(streamCount).toBe(streamsNum); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + await client.destroy({ force: true }); + await server.stop({ force: true }); }); test('destroying stream should clean up on both ends while streams are used', async () => { const message = Buffer.from('Message!'); @@ -133,12 +133,12 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '::' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -185,24 +185,22 @@ describe(QUICStream.name, () => { }), ]); // Start destroying streams - await Promise.allSettled( - streams.map((stream) => stream.destroy({ force: true })), - ); + await Promise.allSettled(streams.map((stream) => stream.destroy())); await Promise.race([ streamEndedProm.p, - testsUtils.sleep(100000).then(() => { + testsUtils.sleep(200).then(() => { throw Error('Ending timed out'); }), ]); expect(streamCreatedCount).toBe(streamsNum); expect(streamEndedCount).toBe(streamsNum); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + await client.destroy({ force: true }); + await server.stop({ force: true }); }); test('should send data over stream', async () => { const message = Buffer.from('The Quick Brown Fox Jumped Over The Lazy Dog'); const numStreams = 10; - const numMessage = 100; + const numMessage = 10; const connectionEventProm = utils.promise(); const tlsConfig = await generateConfig(defaultType); @@ -224,13 +222,13 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, port: 58888 as Port, }); const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '::' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -277,8 +275,8 @@ describe(QUICStream.name, () => { Promise.all(activeClientStreams), Promise.all(activeServerStreams), ]); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + await client.destroy({ force: true }); + await server.stop({ force: true }); }); test('should propagate errors over stream for writable', async () => { const streamsNum = 10; @@ -318,13 +316,13 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, port: 59999 as Port, }); const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '::' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -379,8 +377,8 @@ describe(QUICStream.name, () => { Promise.all(activeClientStreams), Promise.all(expectationProms), ]); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + await client.destroy({ force: true }); + await server.stop({ force: true }); }); test('should propagate errors over stream for readable', async () => { const streamsNum = 1; @@ -420,13 +418,13 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, port: 60000 as Port, }); const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '::' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -490,103 +488,271 @@ describe(QUICStream.name, () => { serverStreamsDoneProm.resolveP(); }), ]); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); + test('should clean up streams when local connection ends', async () => { + const streamsNum = 10; + const message = Buffer.from('The quick brown fox jumped over the lazy dog'); + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: localhost, + }); + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + let streamCreatedCount = 0; + let streamEndedCount = 0; + const streamCreationProm = utils.promise(); + const streamEndedProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (asd: events.QUICConnectionStreamEvent) => { + const stream = asd.detail; + streamCreatedCount += 1; + if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); + void stream.readable + .pipeTo(stream.writable) + // Ignore errors + .catch(() => {}) + .finally(() => { + streamEndedCount += 1; + if (streamEndedCount >= streamsNum) streamEndedProm.resolveP(); + }); + }, + ); + // Let's make a new streams. + for (let i = 0; i < streamsNum; i++) { + const stream = await client.connection.streamNew(); + const writer = stream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + } + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(100).then(() => { + throw Error('Creation timed out'); + }), + ]); + // Start destroying streams + await client.destroy({ force: true }); + // All streams need to finish + await Promise.race([ + streamEndedProm.p, + testsUtils.sleep(100).then(() => { + throw Error('Ending timed out'); + }), + ]); + expect(streamCreatedCount).toBe(streamsNum); + expect(streamEndedCount).toBe(streamsNum); + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); + test('should clean up streams when peer connection ends', async () => { + const streamsNum = 10; + const message = Buffer.from('The quick brown fox jumped over the lazy dog'); + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: localhost, + }); + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + let streamCreatedCount = 0; + let streamEndedCount = 0; + const streamCreationProm = utils.promise(); + const streamEndedProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (asd: events.QUICConnectionStreamEvent) => { + const stream = asd.detail; + streamCreatedCount += 1; + if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); + void stream.readable + .pipeTo(stream.writable) + // Ignore errors + .catch(() => {}) + .finally(() => { + streamEndedCount += 1; + if (streamEndedCount >= streamsNum) streamEndedProm.resolveP(); + }); + }, + ); + // Let's make a new streams. + for (let i = 0; i < streamsNum; i++) { + const stream = await client.connection.streamNew(); + const writer = stream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + } + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(100).then(() => { + throw Error('Creation timed out'); + }), + ]); + // Start destroying streams + await conn.stop({ force: true }); + await Promise.race([ + streamEndedProm.p, + testsUtils.sleep(100).then(() => { + throw Error('Ending timed out'); + }), + ]); + expect(streamCreatedCount).toBe(streamsNum); + expect(streamEndedCount).toBe(streamsNum); + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); + test('should clean up streams when connection times out', async () => { + const streamsNum = 10; + const message = Buffer.from('The quick brown fox jumped over the lazy dog'); + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + maxIdleTimeout: 100, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: localhost, + }); + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + let streamCreatedCount = 0; + let streamEndedCount = 0; + const streamCreationProm = utils.promise(); + const streamEndedProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (asd: events.QUICConnectionStreamEvent) => { + const stream = asd.detail; + streamCreatedCount += 1; + if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); + void stream.readable + .pipeTo(stream.writable) + // Ignore errors + .catch(() => {}) + .finally(() => { + streamEndedCount += 1; + if (streamEndedCount >= streamsNum) streamEndedProm.resolveP(); + }); + }, + ); + // Let's make a new streams. + for (let i = 0; i < streamsNum; i++) { + const stream = await client.connection.streamNew(); + const writer = stream.writable.getWriter(); + await writer.write(message); + writer.releaseLock(); + } + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(100).then(() => { + throw Error('Creation timed out'); + }), + ]); + // Wait for streams to end with timeout + await Promise.race([ + streamEndedProm.p, + testsUtils.sleep(1000).then(() => { + throw Error('Ending timed out'); + }), + ]); + expect(streamCreatedCount).toBe(streamsNum); + expect(streamEndedCount).toBe(streamsNum); + await client.destroy({ force: true }); + await server.stop({ force: true }); }); - testProp( - 'should clean up streams when connection ends', - [ - fc.integer({ min: 5, max: 10 }).noShrink(), - fc.uint8Array({ minLength: 1 }).noShrink(), - ], - async (streamsNum, message) => { - const connectionEventProm = - utils.promise(); - const tlsConfig = await generateConfig(defaultType); - const server = new QUICServer({ - crypto: { - key, - ops: serverCrypto, - }, - logger: logger.getChild(QUICServer.name), - config: { - key: tlsConfig.key, - cert: tlsConfig.cert, - verifyPeer: false, - }, - }); - testsUtils.extractSocket(server, sockets); - server.addEventListener( - 'serverConnection', - (e: events.QUICServerConnectionEvent) => - connectionEventProm.resolveP(e), - ); - await server.start({ - host: '127.0.0.1' as Host, - }); - const client = await QUICClient.createQUICClient({ - host: '::ffff:127.0.0.1' as Host, - port: server.port, - localHost: '::' as Host, - crypto: { - ops: clientCrypto, - }, - logger: logger.getChild(QUICClient.name), - config: { - verifyPeer: false, - }, - }); - testsUtils.extractSocket(client, sockets); - const conn = (await connectionEventProm.p).detail; - // Do the test - let streamCreatedCount = 0; - let streamEndedCount = 0; - const streamCreationProm = utils.promise(); - const streamEndedProm = utils.promise(); - conn.addEventListener( - 'connectionStream', - (asd: events.QUICConnectionStreamEvent) => { - const stream = asd.detail; - streamCreatedCount += 1; - if (streamCreatedCount >= streamsNum) streamCreationProm.resolveP(); - void stream.readable - .pipeTo(stream.writable) - // Ignore errors - .catch(() => {}) - .finally(() => { - streamEndedCount += 1; - if (streamEndedCount >= streamsNum) streamEndedProm.resolveP(); - }); - }, - ); - // Let's make a new streams. - for (let i = 0; i < streamsNum; i++) { - const stream = await client.connection.streamNew(); - const writer = stream.writable.getWriter(); - await writer.write(message); - writer.releaseLock(); - } - await Promise.race([ - streamCreationProm.p, - testsUtils.sleep(100).then(() => { - throw Error('Creation timed out'); - }), - ]); - // Start destroying streams - await client.destroy({ force: true }); - await Promise.race([ - streamEndedProm.p, - testsUtils.sleep(100).then(() => { - throw Error('Ending timed out'); - }), - ]); - expect(streamCreatedCount).toBe(streamsNum); - expect(streamEndedCount).toBe(streamsNum); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); - }, - { numRuns: 2 }, - ); test('streams should contain metadata', async () => { const connectionEventProm = utils.promise(); @@ -603,6 +769,7 @@ describe(QUICStream.name, () => { cert: tlsConfig1.cert, verifyPeer: true, ca: tlsConfig2.ca, + maxIdleTimeout: 100, }, }); testsUtils.extractSocket(server, sockets); @@ -611,12 +778,12 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '127.0.0.1' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -625,6 +792,7 @@ describe(QUICStream.name, () => { verifyPeer: false, key: tlsConfig2.key, cert: tlsConfig2.cert, + maxIdleTimeout: 100, }, }); testsUtils.extractSocket(client, sockets); @@ -658,7 +826,7 @@ describe(QUICStream.name, () => { const clientPemChain = utils.certificatePEMsToCertChainPem( clientMetadata.remoteCertificates!, ); - expect(clientPemChain).toEqual(tlsConfig1.ca); + expect(clientPemChain).toEqual(tlsConfig1.cert); const serverStream = await serverStreamProm.p; const serverMetadata = serverStream.remoteInfo; @@ -670,9 +838,9 @@ describe(QUICStream.name, () => { const serverPemChain = utils.certificatePEMsToCertChainPem( serverMetadata.remoteCertificates!, ); - expect(serverPemChain).toEqual(tlsConfig2.ca); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + expect(serverPemChain).toEqual(tlsConfig2.cert); + await client.destroy({ force: true }); + await server.stop({ force: true }); }); test('streams can be cancelled', async () => { const cancelReason = Symbol('CancelReason'); @@ -699,12 +867,12 @@ describe(QUICStream.name, () => { (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), ); await server.start({ - host: '127.0.0.1' as Host, + host: localhost, }); const client = await QUICClient.createQUICClient({ - host: '127.0.0.1' as Host, + host: localhost, port: server.port, - localHost: '127.0.0.1' as Host, + localHost: localhost, crypto: { ops: clientCrypto, }, @@ -752,7 +920,171 @@ describe(QUICStream.name, () => { // And client stream should've cleaned up await testsUtils.sleep(100); expect(clientStream[destroyed]).toBeTrue(); - await client?.destroy({ force: true }); - await server?.stop({ force: true }); + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); + test('Stream will end when waiting for more data', async () => { + // Needed to check that the pull based reading of data doesn't break when we + // temporarily run out of data to read + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: localhost, + }); + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const streamCreationProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (event: events.QUICConnectionStreamEvent) => { + streamCreationProm.resolveP(event.detail); + }, + ); + const message = Buffer.from('Hello!'); + const clientStream = await client.connection.streamNew(); + const clientWriter = clientStream.writable.getWriter(); + await clientWriter.write(message); + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(500).then(() => { + throw Error('Creation timed out'); + }), + ]); + const serverStream = await streamCreationProm.p; + + // Drain the readable buffer + const serverReader = serverStream.readable.getReader(); + serverReader.releaseLock(); + + // Closing stream with no buffered data should be responsive + await clientWriter.close(); + await serverStream.writable.close(); + + // Both streams are destroyed even without reading till close + await Promise.race([ + Promise.all([clientStream.destroyedP, serverStream.destroyedP]), + utils.sleep(100).then(() => { + throw Error('took too long to destroy'); + }), + ]); + + await client.destroy({ force: true }); + await server.stop({ force: true }); + }); + test('Stream can error when blocked on data', async () => { + // This checks that if the readable web-stream is full and not pulling data, + // we will still respond to an error in the readable stream + + const connectionEventProm = + utils.promise(); + const tlsConfig = await generateConfig(defaultType); + const server = new QUICServer({ + crypto: { + key, + ops: serverCrypto, + }, + logger: logger.getChild(QUICServer.name), + config: { + key: tlsConfig.key, + cert: tlsConfig.cert, + verifyPeer: false, + }, + }); + testsUtils.extractSocket(server, sockets); + server.addEventListener( + 'serverConnection', + (e: events.QUICServerConnectionEvent) => connectionEventProm.resolveP(e), + ); + await server.start({ + host: localhost, + }); + const client = await QUICClient.createQUICClient({ + host: localhost, + port: server.port, + localHost: localhost, + crypto: { + ops: clientCrypto, + }, + logger: logger.getChild(QUICClient.name), + config: { + verifyPeer: false, + }, + }); + testsUtils.extractSocket(client, sockets); + const conn = (await connectionEventProm.p).detail; + // Do the test + const streamCreationProm = utils.promise(); + conn.addEventListener( + 'connectionStream', + (event: events.QUICConnectionStreamEvent) => { + streamCreationProm.resolveP(event.detail); + }, + ); + const message = Buffer.from('Hello!'); + const clientStream = await client.connection.streamNew(); + const clientWriter = clientStream.writable.getWriter(); + await clientWriter.write(message); + await Promise.race([ + streamCreationProm.p, + testsUtils.sleep(500).then(() => { + throw Error('Creation timed out'); + }), + ]); + const serverStream = await streamCreationProm.p; + + // Fill up buffers to block reads from pulling + const serverWriter = serverStream.writable.getWriter(); + await serverWriter.write(message); + await serverWriter.write(message); + await serverWriter.write(message); + await clientWriter.write(message); + await clientWriter.write(message); + await clientWriter.write(message); + + // Closing stream with no buffered data should be responsive + await clientWriter.abort(Error('some error')); + await serverWriter.abort(Error('some error')); + + // Both streams are destroyed even without reading till close + await Promise.race([ + Promise.all([clientStream.destroyedP, serverStream.destroyedP]), + utils.sleep(100).then(() => { + throw Error('took too long to destroy'); + }), + ]); + + await client.destroy({ force: true }); + await server.stop({ force: true }); }); }); diff --git a/tests/concurrency.test.ts b/tests/concurrency.test.ts index 1a57f802..375cfb1c 100644 --- a/tests/concurrency.test.ts +++ b/tests/concurrency.test.ts @@ -145,13 +145,13 @@ describe('Concurrency tests', () => { }); const connProms: Array> = []; server.addEventListener( - 'connection', + 'serverConnection', async (e: events.QUICServerConnectionEvent) => { const conn = e.detail; const connProm = (async () => { const serverStreamProms: Array> = []; conn.addEventListener( - 'stream', + 'connectionStream', (streamEvent: events.QUICConnectionStreamEvent) => { const stream = streamEvent.detail; const streamData = @@ -267,13 +267,13 @@ describe('Concurrency tests', () => { }); const connProms: Array> = []; server.addEventListener( - 'connection', + 'serverConnection', async (e: events.QUICServerConnectionEvent) => { const conn = e.detail; const connProm = (async () => { const serverStreamProms: Array> = []; conn.addEventListener( - 'stream', + 'connectionStream', (streamEvent: events.QUICConnectionStreamEvent) => { const stream = streamEvent.detail; const streamData = @@ -406,13 +406,13 @@ describe('Concurrency tests', () => { }); const connProms: Array> = []; server.addEventListener( - 'connection', + 'serverConnection', async (e: events.QUICServerConnectionEvent) => { const conn = e.detail; const connProm = (async () => { const serverStreamProms: Array> = []; conn.addEventListener( - 'stream', + 'connectionStream', (streamEvent: events.QUICConnectionStreamEvent) => { const stream = streamEvent.detail; const streamData = diff --git a/tests/native/quiche.connection.lifecycle.test.ts b/tests/native/quiche.connection.lifecycle.test.ts index df1da66a..25485eca 100644 --- a/tests/native/quiche.connection.lifecycle.test.ts +++ b/tests/native/quiche.connection.lifecycle.test.ts @@ -103,6 +103,7 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); }); @@ -749,11 +750,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairRSAPEM.privateKey, cert: certRSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1277,11 +1280,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1718,11 +1723,13 @@ describe('quiche connection lifecycle', () => { const clientConfig: QUICConfig = { ...clientDefault, verifyPeer: false, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); diff --git a/tests/native/quiche.tls.test.ts b/tests/native/quiche.tls.test.ts index 92d1a7f0..cf18de20 100644 --- a/tests/native/quiche.tls.test.ts +++ b/tests/native/quiche.tls.test.ts @@ -116,6 +116,7 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -123,6 +124,7 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -318,12 +320,14 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, key: keyPairRSAPEM.privateKey, cert: certRSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -721,6 +725,7 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -728,6 +733,7 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -927,6 +933,7 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -934,6 +941,7 @@ describe('quiche tls', () => { key: keyPairRSAPEM.privateKey, cert: certRSAPEM, ca: certRSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1133,6 +1141,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -1140,6 +1149,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1321,12 +1331,14 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1514,6 +1526,7 @@ describe('quiche tls', () => { verifyPeer: true, key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -1521,6 +1534,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1700,6 +1714,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -1707,6 +1722,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -1893,6 +1909,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -1900,6 +1917,7 @@ describe('quiche tls', () => { key: keyPairECDSAPEM.privateKey, cert: certECDSAPEM, ca: certECDSAPEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -2086,6 +2104,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -2093,6 +2112,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -2274,12 +2294,14 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, verifyPeer: true, key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -2467,6 +2489,7 @@ describe('quiche tls', () => { verifyPeer: true, key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -2474,6 +2497,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -2657,6 +2681,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -2664,6 +2689,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); @@ -2883,6 +2909,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; const serverConfig: QUICConfig = { ...serverDefault, @@ -2890,6 +2917,7 @@ describe('quiche tls', () => { key: keyPairEd25519PEM.privateKey, cert: certEd25519PEM, ca: certEd25519PEM, + maxIdleTimeout: 0, }; clientQuicheConfig = buildQuicheConfig(clientConfig); serverQuicheConfig = buildQuicheConfig(serverConfig); diff --git a/tests/utils.ts b/tests/utils.ts index 25be4097..cfdb228b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -470,7 +470,6 @@ async function generateCertificate({ return await x509.X509CertificateGenerator.create(certConfig); } -// FIXME: // async function createTLSConfigWithChain( // keyPairs: Array<{ // publicKey: JsonWebKey; @@ -751,7 +750,7 @@ export { keyPairECDSAToPEM, keyPairEd25519ToPEM, generateCertificate, - // CreateTLSConfigWithChain, FIXME + // CreateTLSConfigWithChain certToPEM, generateKeyHMAC, signHMAC, From cbedc265c7d9d4e490f3b02c3f7d8d3e9fb54fc5 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Thu, 6 Jul 2023 15:30:51 +1000 Subject: [PATCH 22/22] fix: cleaning up `Monitor` resources --- src/QUICConnection.ts | 78 +++++++++++++++++++++---------------------- src/QUICSocket.ts | 22 +++++++----- src/utils.ts | 16 +++++++++ tests/utils.ts | 2 +- 4 files changed, 70 insertions(+), 48 deletions(-) diff --git a/src/QUICConnection.ts b/src/QUICConnection.ts index a9982194..89a8c389 100644 --- a/src/QUICConnection.ts +++ b/src/QUICConnection.ts @@ -13,7 +13,8 @@ import type { VerifyCallback, } from './types'; import type { Connection, ConnectionErrorCode, SendInfo } from './native/types'; -import { Lock, LockBox, Monitor, RWLockWriter } from '@matrixai/async-locks'; +import type { Monitor } from '@matrixai/async-locks'; +import { Lock, LockBox, RWLockWriter } from '@matrixai/async-locks'; import { ready, running, @@ -515,29 +516,34 @@ class QUICConnection extends EventTarget { this.logger.debug('streams destroyed'); this.stopKeepAliveIntervalTimer(); - mon = mon ?? new Monitor(this.lockbox, RWLockWriter); // Trigger closing connection in the background and await close later. - void mon.withF(this.lockCode, async (mon) => { - // If this is already closed, then `Done` will be thrown - // Otherwise it can send `CONNECTION_CLOSE` frame - // This can be 0x1c close at the QUIC layer or no errors - // Or it can be 0x1d for application close with an error - // Upon receiving a `CONNECTION_CLOSE`, you can send back - // 1 packet containing a `CONNECTION_CLOSE` frame too - // (with `NO_ERROR` code if appropriate) - // It must enter into a draining state, and no other packets can be sent - try { - this.conn.close(applicationError, errorCode, Buffer.from(errorMessage)); - // If we get a `Done` exception we don't bother calling send - // The send only gets sent if the `Done` is not the case - await this.send(mon); - } catch (e) { - // Ignore 'Done' if already closed - if (e.message !== 'Done') { - // No other exceptions are expected - never(); + void utils.withMonitor(mon, this.lockbox, RWLockWriter, async (mon) => { + await mon.withF(this.lockCode, async (mon) => { + // If this is already closed, then `Done` will be thrown + // Otherwise it can send `CONNECTION_CLOSE` frame + // This can be 0x1c close at the QUIC layer or no errors + // Or it can be 0x1d for application close with an error + // Upon receiving a `CONNECTION_CLOSE`, you can send back + // 1 packet containing a `CONNECTION_CLOSE` frame too + // (with `NO_ERROR` code if appropriate) + // It must enter into a draining state, and no other packets can be sent + try { + this.conn.close( + applicationError, + errorCode, + Buffer.from(errorMessage), + ); + // If we get a `Done` exception we don't bother calling send + // The send only gets sent if the `Done` is not the case + await this.send(mon); + } catch (e) { + // Ignore 'Done' if already closed + if (e.message !== 'Done') { + // No other exceptions are expected + never(); + } } - } + }); }); if (this.conn.isClosed()) { @@ -750,17 +756,14 @@ class QUICConnection extends EventTarget { * Any errors must be emitted as events. * @internal */ - public async send( - mon: Monitor = new Monitor( - this.lockbox, - RWLockWriter, - ), - ): Promise { - if (!mon.isLocked(this.lockCode)) { - return mon.withF(this.lockCode, async (mon) => { - return this.send(mon); - }); - } + public async send(mon?: Monitor): Promise { + await utils.withMonitor(mon, this.lockbox, RWLockWriter, async (mon) => { + if (!mon.isLocked(this.lockCode)) { + return mon.withF(this.lockCode, async (mon) => { + return this.send(mon); + }); + } + }); const sendBuffer = new Uint8Array(quiche.MAX_DATAGRAM_SIZE); let sendLength: number; @@ -914,17 +917,15 @@ class QUICConnection extends EventTarget { this.resolveClosedP(); // If we are still running and not stopping then we need to stop if (this[running] && this[status] !== 'stopping') { - const mon = new Monitor(this.lockbox, RWLockWriter); // Background stopping, we don't want to block the timer resolving - void this.stop({ force: true }, mon); + void this.stop({ force: true }); } logger.debug('CLEANING UP TIMER'); return; } - const mon = new Monitor(this.lockbox, RWLockWriter); // There may be data to send after timing out - void this.send(mon); + void this.send(); // Note that a `0` timeout is still a valid timeout const timeout = this.conn.timeout(); @@ -982,9 +983,8 @@ class QUICConnection extends EventTarget { // Intelligently schedule a PING frame. // If the connection has already sent ack-eliciting frames // then this is a noop. - const mon = new Monitor(this.lockbox, RWLockWriter); this.conn.sendAckEliciting(); - await this.send(mon); + await this.send(); this.keepAliveIntervalTimer = new Timer({ delay: ms, handler: keepAliveHandler, diff --git a/src/QUICSocket.ts b/src/QUICSocket.ts index 8b663127..78fd3906 100644 --- a/src/QUICSocket.ts +++ b/src/QUICSocket.ts @@ -6,7 +6,7 @@ import dgram from 'dgram'; import Logger from '@matrixai/logger'; import { running } from '@matrixai/async-init'; import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; -import { Monitor, RWLockWriter } from '@matrixai/async-locks'; +import { RWLockWriter } from '@matrixai/async-locks'; import { status } from '@matrixai/async-init/dist/utils'; import QUICConnectionId from './QUICConnectionId'; import QUICConnectionMap from './QUICConnectionMap'; @@ -107,13 +107,19 @@ class QUICSocket extends EventTarget { // Acquire the conn lock, this ensures mutual exclusion // for state changes on the internal connection try { - const mon = new Monitor(connection.lockbox, RWLockWriter); - await mon.withF(connection.lockCode, async (mon) => { - // Even if we are `stopping`, the `quiche` library says we need to - // continue processing any packets. - await connection.recv(data, remoteInfo_, mon); - await connection.send(mon); - }); + await utils.withMonitor( + undefined, + connection.lockbox, + RWLockWriter, + async (mon) => { + await mon.withF(connection.lockCode, async (mon) => { + // Even if we are `stopping`, the `quiche` library says we need to + // continue processing any packets. + await connection.recv(data, remoteInfo_, mon); + await connection.send(mon); + }); + }, + ); } catch (e) { // Race condition with destroying socket, just ignore if (!(e instanceof errors.ErrorQUICSocketNotRunning)) throw e; diff --git a/src/utils.ts b/src/utils.ts index 283dc652..6812aec4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,8 +8,10 @@ import type { ServerCrypto, } from './types'; import type { Connection } from '@/native'; +import type { LockBox, RWLockWriter } from '@matrixai/async-locks'; import dns from 'dns'; import { IPv4, IPv6, Validator } from 'ip-num'; +import { Monitor } from '@matrixai/async-locks'; import QUICConnectionId from './QUICConnectionId'; import * as errors from './errors'; @@ -428,6 +430,19 @@ function streamStats( `; } +async function withMonitor( + mon: Monitor | undefined, + lockBox: LockBox, + lockConstructor: { new (): RWLockWriter }, + fun: (mon: Monitor) => Promise, + locksPending?: Map, +): Promise { + const _mon = mon ?? new Monitor(lockBox, lockConstructor, locksPending); + const result = await fun(_mon); + if (mon != null) await _mon.unlockAll(); + return result; +} + export { isIPv4, isIPv6, @@ -455,4 +470,5 @@ export { validateToken, sleep, streamStats, + withMonitor, }; diff --git a/tests/utils.ts b/tests/utils.ts index cfdb228b..f04d4388 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -470,7 +470,7 @@ async function generateCertificate({ return await x509.X509CertificateGenerator.create(certConfig); } -// async function createTLSConfigWithChain( +// Async function createTLSConfigWithChain( // keyPairs: Array<{ // publicKey: JsonWebKey; // privateKey: JsonWebKey;