From fc9f3f6d2a8d3231c98c9066279f061d19031f86 Mon Sep 17 00:00:00 2001 From: Ryan Hurey Date: Thu, 2 Jul 2020 09:59:35 -0700 Subject: [PATCH] Add a cache for OCSP responses. (#196) * Extend OCSP certificate verification to include a memory and disk based cache. The disk cache will by default be located in TMPDIR// but can be overridden. When the cached OCSP response is within 1 day of expiring, or 1/2 way through it's vaility period (whichever is shorter) a background task is queued to refresh it while the cached value is used. * More fixes * Fix error case * Fix typos --- external/ocsp/ocsp.d.ts | 39 +- package-lock.json | 281 +++++++++++-- package.json | 14 +- src/common.browser/CertChecks.ts | 370 ++++++++++++++++++ src/common.browser/WebsocketMessageAdapter.ts | 72 +--- src/common/Exports.ts | 1 + src/common/OCSPEvents.ts | 126 ++++++ tests/AutoSourceLangDetectionTests.ts | 2 +- tests/ConnectionTests.ts | 2 +- tests/ConversationTranslatorTests.ts | 2 +- tests/DialogServiceConnectorTests.ts | 2 +- tests/IntentRecognizerTests.ts | 2 +- .../SpeechRecoAuthTokenErrorMessageTests.ts | 2 +- .../SpeechRecoAuthTokenRefreshTests.ts | 2 +- tests/LongRunning/SpeechRecoReconnectTests.ts | 2 +- .../TranslationRecoReconnectTests.ts | 2 +- tests/OCSPCacheTests.ts | 251 ++++++++++++ tests/SpeechRecognizerTests.ts | 2 +- tests/SpeechSynthesisTests.ts | 2 +- tests/TranslationRecognizerTests.ts | 2 +- tests/TranslationSynthTests.ts | 2 +- tests/Utilities.ts | 26 +- tsconfig.json | 3 +- 23 files changed, 1095 insertions(+), 114 deletions(-) create mode 100644 src/common.browser/CertChecks.ts create mode 100644 src/common/OCSPEvents.ts create mode 100644 tests/OCSPCacheTests.ts diff --git a/external/ocsp/ocsp.d.ts b/external/ocsp/ocsp.d.ts index 30f4622..5cce43f 100644 --- a/external/ocsp/ocsp.d.ts +++ b/external/ocsp/ocsp.d.ts @@ -1,12 +1,47 @@ /** Declaration file generated by dts-gen */ -import { Certificate, Hash } from "crypto"; import * as http from "http"; -import { DetailedPeerCertificate } from "tls"; +import { DetailedPeerCertificate, Certificate, TlsOptions } from "tls"; +import { HttpsProxyAgentOptions } from "https-proxy-agent"; export class Agent extends http.Agent { public constructor(options: any); + public fetchIssuer(peerCert: DetailedPeerCertificate, stapling: Certificate, cb: (error: string, result: DetailedPeerCertificate) => void): void; } export function check(options: any, cb: (error: Error, res: any) => void): any; +export function verify(options: VerifyOptions, cb: (error: string, res: any) => void): void; + +export interface Response { + start: any; + end: any; + value: any; + certs: any; + certsTbs: any; +} + +export interface Request { + id: Buffer; + certID: any; + data: any; + + // Just to avoid re-parsing DER + cert: any; + issuer: any; +} + +export interface VerifyOptions { + request: Request; + response: Buffer; +} + +export class utils { + public static parseResponse(response: any): Response; + public static getAuthorityInfo(cert: DetailedPeerCertificate, ocspMethod: string, cb: (err: string, uri: string) => void): void; + public static getResponse(httpOptions: http.RequestOptions | string, data: any, cb: (err: string, raw: Buffer) => void): void; +} + +export class request { + public static generate(rawCert: Buffer, rawIssuer: Buffer): Request; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb3e206..7539cf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1799,6 +1799,16 @@ "form-data": "^2.5.0" } }, + "@types/rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -2059,11 +2069,21 @@ "dev": true }, "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", "requires": { - "es6-promisify": "^5.0.0" + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } } }, "ajv": { @@ -2377,6 +2397,51 @@ "lodash": "^4.17.14" } }, + "async-disk-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/async-disk-cache/-/async-disk-cache-2.1.0.tgz", + "integrity": "sha512-iH+boep2xivfD9wMaZWkywYIURSmsL96d6MoqrC94BnGSvXE4Quf8hnJiHGFYhw/nLeIa1XyRaf4vvcvkwAefg==", + "requires": { + "debug": "^4.1.1", + "heimdalljs": "^0.2.3", + "istextorbinary": "^2.5.1", + "mkdirp": "^0.5.0", + "rimraf": "^3.0.0", + "rsvp": "^4.8.5", + "username-sync": "^1.0.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -2600,8 +2665,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2685,6 +2749,11 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "binaryextensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", + "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==" + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -2710,7 +2779,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2944,6 +3012,15 @@ "path-is-absolute": "^1.0.0" } }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", @@ -3225,8 +3302,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -3273,6 +3349,31 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "copy-descriptor": { @@ -3724,6 +3825,15 @@ "safer-buffer": "^2.1.0" } }, + "editions": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/editions/-/editions-2.3.1.tgz", + "integrity": "sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==", + "requires": { + "errlop": "^2.0.0", + "semver": "^6.3.0" + } + }, "elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", @@ -3783,6 +3893,11 @@ } } }, + "errlop": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/errlop/-/errlop-2.2.0.tgz", + "integrity": "sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==" + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -4456,8 +4571,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.13", @@ -4920,6 +5034,21 @@ "minimalistic-assert": "^1.0.1" } }, + "heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "requires": { + "rsvp": "~3.2.1" + }, + "dependencies": { + "rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha1-B8tKXfJa3Z6Cbrxn3Mn9idsn2Eo=" + } + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -4985,6 +5114,16 @@ "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + } } }, "human-signals": { @@ -5085,7 +5224,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5480,6 +5618,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "istextorbinary": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.6.0.tgz", + "integrity": "sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==", + "requires": { + "binaryextensions": "^2.1.2", + "editions": "^2.2.0", + "textextensions": "^2.5.0" + } + }, "jest": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/jest/-/jest-26.0.1.tgz", @@ -9082,7 +9230,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9148,7 +9295,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, "requires": { "minimist": "^1.2.5" }, @@ -9156,8 +9302,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, @@ -9173,6 +9318,31 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "ms": { @@ -9508,11 +9678,60 @@ "make-iterator": "^1.0.0" } }, + "ocsp": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ocsp/-/ocsp-1.2.0.tgz", + "integrity": "sha1-RpoXdrRX3uZ+sCAUCMGUa6xAdsw=", + "dev": true, + "requires": { + "asn1.js": "^4.8.0", + "asn1.js-rfc2560": "^4.0.0", + "asn1.js-rfc5280": "^2.0.0", + "async": "^1.5.2", + "simple-lru-cache": "0.0.2" + }, + "dependencies": { + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "asn1.js-rfc2560": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/asn1.js-rfc2560/-/asn1.js-rfc2560-4.0.6.tgz", + "integrity": "sha512-ysf48ni+f/efNPilq4+ApbifUPcSW/xbDeQAh055I+grr2gXgNRQqHew7kkO70WSMQ2tEOURVwsK+dJqUNjIIg==", + "dev": true, + "requires": { + "asn1.js-rfc5280": "^2.0.0" + } + }, + "asn1.js-rfc5280": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/asn1.js-rfc5280/-/asn1.js-rfc5280-2.0.1.tgz", + "integrity": "sha512-1e2ypnvTbYD/GdxWK77tdLBahvo1fZUHlQJqAVUuZWdYj0rdjGcf2CWYUtbsyRYpYUMwMWLZFUtLxog8ZXTrcg==", + "dev": true, + "requires": { + "asn1.js": "^4.5.0" + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -9712,8 +9931,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "2.0.1", @@ -10273,9 +10491,9 @@ "dev": true }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -10310,8 +10528,7 @@ "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, "run-queue": { "version": "1.0.3", @@ -10391,8 +10608,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "semver-greatest-satisfied-range": { "version": "1.1.0", @@ -11084,6 +11300,11 @@ } } }, + "textextensions": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.6.0.tgz", + "integrity": "sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==" + }, "throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -11599,6 +11820,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "username-sync": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.2.tgz", + "integrity": "sha512-ayNkOJdoNSGNDBE46Nkc+l6IXmeugbzahZLSMkwvgRWv5y5ZqNY2IrzcgmkR4z32sj1W3tM3TuTUMqkqBzO+RA==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -11947,8 +12173,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "3.0.3", diff --git a/package.json b/package.json index c4533ed..a124f01 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,16 @@ "browser": { "asn1.js-rfc2560": false, "asn1.js-rfc5280": false, - "distrib/es2015/external/ocsp/ocsp": false, - "distrib/lib/external/ocsp/ocsp": false, "https-proxy-agent": false, "simple-lru-cache": false, "ws": false, "fs": false, - "xmlhttprequest-ts": false + "xmlhttprequest-ts": false, + "async-disk-cache": false, + "distrib/es2015/external/ocsp/ocsp": false, + "distrib/lib/external/ocsp/ocsp": false, + "agent-base": false, + "tls": false }, "main": "distrib/lib/microsoft.cognitiveservices.speech.sdk.js", "module": "distrib/es2015/microsoft.cognitiveservices.speech.sdk.js", @@ -51,6 +54,7 @@ "@types/jest": "^25.2.2", "@types/node": "^12.12.30", "@types/request": "^2.48.3", + "@types/rimraf": "^3.0.0", "@types/ws": "^6.0.4", "asn1.js": "^5.2.0", "dts-bundle-webpack": "^1.0.2", @@ -62,7 +66,9 @@ "gulp-typescript": "^5.0.1", "jest": "^26.0.1", "jest-junit": "^10.0.0", + "ocsp": "^1.2.0", "request": "^2.88.0", + "rimraf": "^3.0.2", "semver": "^6.3.0", "source-map-loader": "^0.2.4", "ts-jest": "^25.5.1", @@ -90,8 +96,10 @@ "usePathForSuiteName": "true" }, "dependencies": { + "agent-base": "^6.0.0", "asn1.js-rfc2560": "^5.0.0", "asn1.js-rfc5280": "^3.0.0", + "async-disk-cache": "^2.1.0", "https-proxy-agent": "^3.0.1", "simple-lru-cache": "0.0.2", "ws": "^7.2.0", diff --git a/src/common.browser/CertChecks.ts b/src/common.browser/CertChecks.ts new file mode 100644 index 0000000..afb926d --- /dev/null +++ b/src/common.browser/CertChecks.ts @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as http from "http"; +import * as tls from "tls"; +import * as url from "url"; +import * as ocsp from "../../external/ocsp/ocsp"; +import { + Events, + OCSPCacheEntryExpiredEvent, + OCSPCacheEntryNeedsRefreshEvent, + OCSPCacheFetchErrorEvent, + OCSPCacheMissEvent, + OCSPCacheUpdatehCompleteEvent, + OCSPCacheUpdateNeededEvent, + OCSPDiskCacheHitEvent, + OCSPDiskCacheStoreEvent, + OCSPEvent, + OCSPMemoryCacheHitEvent, + OCSPMemoryCacheStoreEvent, + OCSPResponseRetrievedEvent, + OCSPStapleReceivedEvent, + OCSPVerificationFailedEvent, + OCSPWSUpgradeStartedEvent +} from "../common/Exports"; +import { IStringDictionary } from "../common/IDictionary"; +import { ProxyInfo } from "./ProxyInfo"; + +import Agent from "agent-base"; + +// @ts-ignore +import Cache from "async-disk-cache"; +import HttpsProxyAgent from "https-proxy-agent"; +import * as net from "net"; + +export class CertCheckAgent { + + // Test hook to enable forcing expiration / refresh to happen. + public static testTimeOffset: number; + + // Test hook to disable stapling for cache testing. + public static forceDisableOCSPStapling: boolean = false; + + // An in memory cache for recived responses. + private static privMemCache: IStringDictionary = {}; + + // The on disk cache. + private static privDiskCache: Cache; + + private privProxyInfo: ProxyInfo; + + constructor(proxyInfo?: ProxyInfo) { + if (!!proxyInfo) { + this.privProxyInfo = proxyInfo; + } + + // Initialize this here to allow tests to set the env variable before the cache is constructed. + if (!CertCheckAgent.privDiskCache) { + CertCheckAgent.privDiskCache = new Cache("microsoft-cognitiveservices-speech-sdk-cache", { supportBuffer: true, location: (typeof process !== "undefined" && !!process.env.SPEECH_OCSP_CACHE_ROOT) ? process.env.SPEECH_OCSP_CACHE_ROOT : undefined }); + } + } + + // Test hook to force the disk cache to be recreated. + public static forceReinitDiskCache(): void { + CertCheckAgent.privDiskCache = undefined; + CertCheckAgent.privMemCache = {}; + } + + public GetAgent(disableStapling?: boolean): http.Agent { + const agent: any = new Agent.Agent(this.CreateConnection); + + if (this.privProxyInfo !== undefined && + this.privProxyInfo.HostName !== undefined && + this.privProxyInfo.Port > 0) { + const proxyName: string = "privProxyInfo"; + agent[proxyName] = this.privProxyInfo; + } + + return agent; + } + + private static GetProxyAgent(proxyInfo: ProxyInfo): HttpsProxyAgent { + const httpProxyOptions: HttpsProxyAgent.HttpsProxyAgentOptions = { + host: proxyInfo.HostName, + port: proxyInfo.Port, + }; + + if (!!proxyInfo.UserName) { + httpProxyOptions.headers = { + "Proxy-Authentication": "Basic " + new Buffer(proxyInfo.UserName + ":" + (proxyInfo.Password === undefined) ? "" : proxyInfo.Password).toString("base64"), + }; + } else { + httpProxyOptions.headers = {}; + } + + httpProxyOptions.headers.requestOCSP = "true"; + + const httpProxyAgent: HttpsProxyAgent = new HttpsProxyAgent(httpProxyOptions); + return httpProxyAgent; + } + + private static async OCSPCheck(socketPromise: Promise, proxyInfo: ProxyInfo): Promise { + let ocspRequest: ocsp.Request; + let stapling: Buffer; + let resolved: boolean = false; + + const socket: net.Socket = await socketPromise; + socket.cork(); + + const tlsSocket: tls.TLSSocket = socket as tls.TLSSocket; + + return new Promise((resolve: (value: net.Socket) => void, reject: (error: string | Error) => void) => { + socket.on("OCSPResponse", (data: Buffer): void => { + if (!!data) { + this.onEvent(new OCSPStapleReceivedEvent()); + stapling = data; + } + }); + + socket.on("error", (error: Error) => { + if (!resolved) { + resolved = true; + socket.destroy(); + reject(error); + } + }); + + tlsSocket.on("secure", async () => { + const peer: tls.DetailedPeerCertificate = tlsSocket.getPeerCertificate(true); + try { + const issuer: tls.DetailedPeerCertificate = await this.GetIssuer(peer); + + // We always need a request to verify the response. + ocspRequest = ocsp.request.generate(peer.raw, issuer.raw); + + // Do we have a result for this certificate in our memory cache? + const sig: string = ocspRequest.id.toString("hex"); + + // Stapled response trumps cached response. + if (!stapling) { + const cacheEntry: Buffer = await CertCheckAgent.GetResponseFromCache(sig, ocspRequest, proxyInfo); + stapling = cacheEntry; + } + + await this.VerifyOCSPResponse(stapling, ocspRequest, proxyInfo); + + socket.uncork(); + resolved = true; + resolve(socket); + } catch (e) { + socket.destroy(); + resolved = true; + reject(e); + } + }); + }); + } + + private static GetIssuer(peer: tls.DetailedPeerCertificate): Promise { + if (peer.issuerCertificate) { + return Promise.resolve(peer.issuerCertificate); + } + + return new Promise((resolve: (value: tls.DetailedPeerCertificate) => void, reject: (reason: string) => void) => { + const ocspAgent: ocsp.Agent = new ocsp.Agent({}); + ocspAgent.fetchIssuer(peer, null, (error: string, value: tls.DetailedPeerCertificate): void => { + if (!!error) { + reject(error); + return; + } + + resolve(value); + }); + }); + } + + private static async GetResponseFromCache(signature: string, ocspRequest: ocsp.Request, proxyInfo: ProxyInfo): Promise { + let cachedResponse: Buffer = CertCheckAgent.privMemCache[signature]; + + if (!!cachedResponse) { + this.onEvent(new OCSPMemoryCacheHitEvent(signature)); + } + + // Do we have a result for this certificate on disk in %TMP%? + if (!cachedResponse) { + try { + const diskCacheResponse: any = await CertCheckAgent.privDiskCache.get(signature); + if (!!diskCacheResponse.isCached) { + CertCheckAgent.onEvent(new OCSPDiskCacheHitEvent(signature)); + CertCheckAgent.StoreMemoryCacheEntry(signature, diskCacheResponse.value); + cachedResponse = diskCacheResponse.value; + } + } catch (error) { + cachedResponse = null; + } + } + + if (!cachedResponse) { + return cachedResponse; + } + + try { + const cachedOcspResponse: ocsp.Response = ocsp.utils.parseResponse(cachedResponse); + const tbsData = cachedOcspResponse.value.tbsResponseData; + if (tbsData.responses.length < 1) { + this.onEvent(new OCSPCacheFetchErrorEvent(signature, "Not enough data in cached response")); + return; + } + + const cachedStartTime: number = tbsData.responses[0].thisUpdate; + const cachedNextTime: number = tbsData.responses[0].nextUpdate; + + if (cachedNextTime < (Date.now() + this.testTimeOffset - 60000)) { + // Cached entry has expired. + this.onEvent(new OCSPCacheEntryExpiredEvent(signature, cachedNextTime)); + cachedResponse = null; + } else { + // If we're within one day of the next update, or 50% of the way through the validity period, + // background an update to the cache. + + const minUpdate: number = Math.min(24 * 60 * 60 * 1000, (cachedNextTime - cachedStartTime) / 2); + + if ((cachedNextTime - (Date.now() + this.testTimeOffset)) < minUpdate) { + this.onEvent(new OCSPCacheEntryNeedsRefreshEvent(signature, cachedStartTime, cachedNextTime)); + this.UpdateCache(ocspRequest, proxyInfo).catch(); + } + } + } catch (error) { + this.onEvent(new OCSPCacheFetchErrorEvent(signature, error)); + cachedResponse = null; + } + if (!cachedResponse) { + this.onEvent(new OCSPCacheMissEvent(signature)); + } + return cachedResponse; + } + + private static async VerifyOCSPResponse(cacheValue: Buffer, ocspRequest: ocsp.Request, proxyInfo: ProxyInfo): Promise { + let ocspResponse: Buffer = cacheValue; + const sig: string = ocspRequest.certID.toString("hex"); + + // Do we have a valid response? + if (!ocspResponse) { + ocspResponse = await CertCheckAgent.GetOCSPResponse(ocspRequest, proxyInfo); + } + + return new Promise((resolve: () => void, reject: (error: string | Error) => void) => { + ocsp.verify({ request: ocspRequest, response: ocspResponse }, (error: string, result: any): void => { + if (!!error) { + CertCheckAgent.onEvent(new OCSPVerificationFailedEvent(ocspRequest.id.toString("hex"), error)); + + // Bad Cached Value? One more try without the cache. + if (!!cacheValue) { + this.VerifyOCSPResponse(null, ocspRequest, proxyInfo).then(() => { + resolve(); + }, (error: Error) => { + reject(error); + }); + } + + reject(error); + } else { + if (!cacheValue) { + CertCheckAgent.StoreCacheEntry(ocspRequest.id.toString("hex"), ocspResponse); + } + resolve(); + } + }); + }); + } + + private static async UpdateCache(req: ocsp.Request, proxyInfo: ProxyInfo): Promise { + const signature: string = req.id.toString("hex"); + this.onEvent(new OCSPCacheUpdateNeededEvent(signature)); + + const rawResponse: Buffer = await this.GetOCSPResponse(req, proxyInfo); + this.StoreCacheEntry(signature, rawResponse); + this.onEvent(new OCSPCacheUpdatehCompleteEvent(req.id.toString("hex"))); + + } + + private static StoreCacheEntry(sig: string, rawResponse: Buffer): void { + this.StoreMemoryCacheEntry(sig, rawResponse); + this.StoreDiskCacheEntry(sig, rawResponse); + } + + private static StoreMemoryCacheEntry(sig: string, rawResponse: Buffer): void { + this.privMemCache[sig] = rawResponse; + this.onEvent(new OCSPMemoryCacheStoreEvent(sig)); + } + + private static StoreDiskCacheEntry(sig: string, rawResponse: Buffer): void { + this.privDiskCache.set(sig, rawResponse).then(() => { + this.onEvent(new OCSPDiskCacheStoreEvent(sig)); + }); + } + + private static GetOCSPResponse(req: ocsp.Request, proxyInfo: ProxyInfo): Promise { + + const ocspMethod: string = "1.3.6.1.5.5.7.48.1"; + let options: http.RequestOptions = {}; + + if (!!proxyInfo) { + const agent: HttpsProxyAgent = CertCheckAgent.GetProxyAgent(proxyInfo); + options.agent = agent; + } + + return new Promise((resolve: (value: Buffer) => void, reject: (error: string | Error) => void) => { + ocsp.utils.getAuthorityInfo(req.cert, ocspMethod, (error: string, uri: string): void => { + if (error) { + reject(error); + return; + } + + const parsedUri = url.parse(uri); + options = { ...options, ...parsedUri }; + + ocsp.utils.getResponse(options, req.data, (error: string, raw: Buffer): void => { + if (error) { + reject(error); + return; + } + + this.onEvent(new OCSPResponseRetrievedEvent(req.certID.toString("hex"))); + resolve(raw); + }); + }); + }); + } + + private static onEvent = (event: OCSPEvent): void => { + Events.instance.onEvent(event); + } + + private CreateConnection(request: Agent.ClientRequest, options: Agent.RequestOptions): Promise { + const enableOCSP: boolean = (typeof process !== "undefined" && process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0" && process.env.SPEECH_CONDUCT_OCSP_CHECK !== "0") && options.secureEndpoint; + let socketPromise: Promise; + + options = { + ...options, + ...{ + requestOCSP: !CertCheckAgent.forceDisableOCSPStapling, + servername: options.host + } + }; + + if (!!this.privProxyInfo) { + const httpProxyAgent: HttpsProxyAgent = CertCheckAgent.GetProxyAgent(this.privProxyInfo); + const baseAgent: Agent.Agent = httpProxyAgent as unknown as Agent.Agent; + + socketPromise = new Promise((resolve: (value: net.Socket) => void, reject: (error: string | Error) => void) => { + baseAgent.callback(request, options, (error: Error, socket: net.Socket) => { + if (!!error) { + reject(error); + } else { + resolve(socket); + } + }); + }); + } else { + socketPromise = Promise.resolve(tls.connect(options)); + } + + if (!!enableOCSP) { + return CertCheckAgent.OCSPCheck(socketPromise, this.privProxyInfo); + } else { + return socketPromise; + } + } +} diff --git a/src/common.browser/WebsocketMessageAdapter.ts b/src/common.browser/WebsocketMessageAdapter.ts index dd263ee..83e1b1b 100644 --- a/src/common.browser/WebsocketMessageAdapter.ts +++ b/src/common.browser/WebsocketMessageAdapter.ts @@ -26,11 +26,8 @@ import { import { ProxyInfo } from "./ProxyInfo"; // Node.JS specific web socket / browser support. -import * as http from "http"; -import * as HttpsProxyAgent from "https-proxy-agent"; -import * as tls from "tls"; -import * as ws from "ws"; -import * as ocsp from "../../external/ocsp/ocsp"; +import ws from "ws"; +import { CertCheckAgent } from "./CertChecks"; interface ISendItem { Message: ConnectionMessage; @@ -101,7 +98,6 @@ export class WebsocketMessageAdapter { this.privConnectionState = ConnectionState.Connecting; try { - const enableOCSP: boolean = (typeof process !== "undefined" && process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0" && process.env.SPEECH_CONDUCT_OCSP_CHECK !== "0") && !this.privUri.startsWith("ws:"); if (typeof WebSocket !== "undefined" && !WebsocketMessageAdapter.forceNpmWebSocket) { // Browser handles cert checks. @@ -109,65 +105,13 @@ export class WebsocketMessageAdapter { this.privWebsocketClient = new WebSocket(this.privUri); } else { - if (this.proxyInfo !== undefined && - this.proxyInfo.HostName !== undefined && - this.proxyInfo.Port > 0) { - const httpProxyOptions: HttpsProxyAgent.HttpsProxyAgentOptions = { - host: this.proxyInfo.HostName, - port: this.proxyInfo.Port, - }; + const options: ws.ClientOptions = { headers: this.privHeaders }; + // The ocsp library will handle validation for us and fail the connection if needed. + this.privCertificateValidatedDeferral.resolve(true); + const checkAgent: CertCheckAgent = new CertCheckAgent(this.proxyInfo); - if (undefined !== this.proxyInfo.UserName) { - httpProxyOptions.headers = { - "Proxy-Authentication": "Basic " + new Buffer(this.proxyInfo.UserName + ":" + (this.proxyInfo.Password === undefined) ? "" : this.proxyInfo.Password).toString("base64"), - }; - if (enableOCSP) { - httpProxyOptions.headers.requestOCSP = "true"; - } - } - - const httpProxyAgent: HttpsProxyAgent = new HttpsProxyAgent(httpProxyOptions); - const httpsOptions: http.RequestOptions = { agent: httpProxyAgent, headers: this.privHeaders }; - - this.privWebsocketClient = new ws(this.privUri, httpsOptions as ws.ClientOptions); - - // Register to be notified when WebSocket upgrade happens so we can check the validity of the - // Certificate. - if (enableOCSP) { - this.privWebsocketClient.addListener("upgrade", (e: http.IncomingMessage): void => { - const tlsSocket: tls.TLSSocket = e.socket as tls.TLSSocket; - const peer: tls.DetailedPeerCertificate = tlsSocket.getPeerCertificate(true); - - // Cork the socket until we know if the cert is good. - tlsSocket.cork(); - - ocsp.check({ - cert: peer.raw, - httpOptions: httpsOptions, - issuer: peer.issuerCertificate.raw, - }, (error: Error, res: any): void => { - if (error) { - this.privCertificateValidatedDeferral.reject(error.message); - tlsSocket.destroy(error); - } else { - this.privCertificateValidatedDeferral.resolve(true); - tlsSocket.uncork(); - } - }); - }); - } else { - this.privCertificateValidatedDeferral.resolve(true); - } - - } else { - const options: ws.ClientOptions = { headers: this.privHeaders }; - // The ocsp library will handle validation for us and fail the connection if needed. - this.privCertificateValidatedDeferral.resolve(true); - if (enableOCSP) { - options.agent = new ocsp.Agent({}); - } - this.privWebsocketClient = new ws(this.privUri, options); - } + options.agent = checkAgent.GetAgent(); + this.privWebsocketClient = new ws(this.privUri, options); } this.privWebsocketClient.binaryType = "arraybuffer"; diff --git a/src/common/Exports.ts b/src/common/Exports.ts index bbaf2bd..673893f 100644 --- a/src/common/Exports.ts +++ b/src/common/Exports.ts @@ -28,3 +28,4 @@ export * from "./Stream"; export { TranslationStatus } from "../common.speech/TranslationStatus"; export * from "./ChunkedArrayBufferStream"; export * from "./IAudioDestination"; +export * from "./OCSPEvents"; diff --git a/src/common/OCSPEvents.ts b/src/common/OCSPEvents.ts new file mode 100644 index 0000000..f5f96ad --- /dev/null +++ b/src/common/OCSPEvents.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { EventType, PlatformEvent } from "./PlatformEvent"; + +export class OCSPEvent extends PlatformEvent { + private privSignature: string; + + constructor(eventName: string, eventType: EventType, signature: string) { + super(eventName, eventType); + + this.privSignature = signature; + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPMemoryCacheHitEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPMemoryCacheHitEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPCacheMissEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPCacheMissEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPDiskCacheHitEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPDiskCacheHitEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPCacheUpdateNeededEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPCacheUpdateNeededEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPMemoryCacheStoreEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPMemoryCacheStoreEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPDiskCacheStoreEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPDiskCacheStoreEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPCacheUpdatehCompleteEvent extends OCSPEvent { + constructor(signature: string) { + super("OCSPCacheUpdatehCompleteEvent", EventType.Debug, signature); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPStapleReceivedEvent extends OCSPEvent { + constructor() { + super("OCSPStapleReceivedEvent", EventType.Debug, ""); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPWSUpgradeStartedEvent extends OCSPEvent { + constructor(serialNumber: string) { + super("OCSPWSUpgradeStartedEvent", EventType.Debug, serialNumber); + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPCacheEntryExpiredEvent extends OCSPEvent { + private privExpireTime: number; + + constructor(serialNumber: string, expireTime: number) { + super("OCSPCacheEntryExpiredEvent", EventType.Debug, serialNumber); + this.privExpireTime = expireTime; + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPCacheEntryNeedsRefreshEvent extends OCSPEvent { + private privExpireTime: number; + private privStartTime: number; + + constructor(serialNumber: string, startTime: number, expireTime: number) { + super("OCSPCacheEntryNeedsRefreshEvent", EventType.Debug, serialNumber); + this.privExpireTime = expireTime; + this.privStartTime = startTime; + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPVerificationFailedEvent extends OCSPEvent { + private privError: string; + + constructor(serialNumber: string, error: string) { + super("OCSPVerificationFailedEvent", EventType.Debug, serialNumber); + this.privError = error; + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPCacheFetchErrorEvent extends OCSPEvent { + private privError: string; + + constructor(serialNumber: string, error: string) { + super("OCSPCacheFetchErrorEvent", EventType.Debug, serialNumber); + this.privError = error; + } +} + +// tslint:disable-next-line:max-classes-per-file +export class OCSPResponseRetrievedEvent extends OCSPEvent { + constructor(serialNumber: string) { + super("OCSPResponseRetrievedEvent", EventType.Debug, serialNumber); + } +} diff --git a/tests/AutoSourceLangDetectionTests.ts b/tests/AutoSourceLangDetectionTests.ts index 7fa4e14..2647849 100644 --- a/tests/AutoSourceLangDetectionTests.ts +++ b/tests/AutoSourceLangDetectionTests.ts @@ -11,7 +11,7 @@ import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; import * as fs from "fs"; import { setTimeout } from "timers"; -import WaitForCondition from "./Utilities"; +import { WaitForCondition } from "./Utilities"; let objsToClose: any[]; diff --git a/tests/ConnectionTests.ts b/tests/ConnectionTests.ts index 69404ac..291d271 100644 --- a/tests/ConnectionTests.ts +++ b/tests/ConnectionTests.ts @@ -13,7 +13,7 @@ import { import { Settings } from "./Settings"; -import WaitForCondition from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; diff --git a/tests/ConversationTranslatorTests.ts b/tests/ConversationTranslatorTests.ts index 913ceff..dc89c9f 100644 --- a/tests/ConversationTranslatorTests.ts +++ b/tests/ConversationTranslatorTests.ts @@ -14,7 +14,7 @@ import { EventType, } from "../src/common/Exports"; import { Settings } from "./Settings"; -import WaitForCondition from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; // tslint:disable-next-line:no-console diff --git a/tests/DialogServiceConnectorTests.ts b/tests/DialogServiceConnectorTests.ts index abcfd9b..5d18e60 100644 --- a/tests/DialogServiceConnectorTests.ts +++ b/tests/DialogServiceConnectorTests.ts @@ -12,7 +12,7 @@ import { } from "../src/common/Exports"; import { PropertyId, PullAudioOutputStream } from "../src/sdk/Exports"; import { Settings } from "./Settings"; -import WaitForCondition from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; // tslint:disable-next-line:no-console diff --git a/tests/IntentRecognizerTests.ts b/tests/IntentRecognizerTests.ts index 3bafedf..b180d1b 100644 --- a/tests/IntentRecognizerTests.ts +++ b/tests/IntentRecognizerTests.ts @@ -8,7 +8,7 @@ import { Events, EventType } from "../src/common/Exports"; import { ByteBufferAudioFile } from "./ByteBufferAudioFile"; import { Settings } from "./Settings"; -import { default as WaitForCondition } from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; import { AudioStreamFormatImpl } from "../src/sdk/Audio/AudioStreamFormat"; diff --git a/tests/LongRunning/SpeechRecoAuthTokenErrorMessageTests.ts b/tests/LongRunning/SpeechRecoAuthTokenErrorMessageTests.ts index 7cdaf4d..76b5ea7 100644 --- a/tests/LongRunning/SpeechRecoAuthTokenErrorMessageTests.ts +++ b/tests/LongRunning/SpeechRecoAuthTokenErrorMessageTests.ts @@ -9,7 +9,7 @@ import { WaveFileAudioInput } from "../WaveFileAudioInputStream"; import * as request from "request"; -import WaitForCondition from "../Utilities"; +import { WaitForCondition } from "../Utilities"; let objsToClose: any[]; diff --git a/tests/LongRunning/SpeechRecoAuthTokenRefreshTests.ts b/tests/LongRunning/SpeechRecoAuthTokenRefreshTests.ts index fb435b8..d346049 100644 --- a/tests/LongRunning/SpeechRecoAuthTokenRefreshTests.ts +++ b/tests/LongRunning/SpeechRecoAuthTokenRefreshTests.ts @@ -9,7 +9,7 @@ import { WaveFileAudioInput } from "../WaveFileAudioInputStream"; import * as request from "request"; -import WaitForCondition from "../Utilities"; +import { WaitForCondition } from "../Utilities"; let objsToClose: any[]; diff --git a/tests/LongRunning/SpeechRecoReconnectTests.ts b/tests/LongRunning/SpeechRecoReconnectTests.ts index 0e416b9..98fe2b0 100644 --- a/tests/LongRunning/SpeechRecoReconnectTests.ts +++ b/tests/LongRunning/SpeechRecoReconnectTests.ts @@ -7,7 +7,7 @@ import { Events, EventType, PlatformEvent } from "../../src/common/Exports"; import { Settings } from "../Settings"; import { WaveFileAudioInput } from "../WaveFileAudioInputStream"; -import WaitForCondition from "../Utilities"; +import { WaitForCondition } from "../Utilities"; let objsToClose: any[]; diff --git a/tests/LongRunning/TranslationRecoReconnectTests.ts b/tests/LongRunning/TranslationRecoReconnectTests.ts index 1c8dcb9..b2a03bb 100644 --- a/tests/LongRunning/TranslationRecoReconnectTests.ts +++ b/tests/LongRunning/TranslationRecoReconnectTests.ts @@ -9,7 +9,7 @@ import { WaveFileAudioInput } from "../WaveFileAudioInputStream"; import * as request from "request"; -import WaitForCondition from "../Utilities"; +import { WaitForCondition } from "../Utilities"; let objsToClose: any[]; diff --git a/tests/OCSPCacheTests.ts b/tests/OCSPCacheTests.ts new file mode 100644 index 0000000..73d069f --- /dev/null +++ b/tests/OCSPCacheTests.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { CertCheckAgent } from "../src/common.browser/CertChecks"; +import { + ConsoleLoggingListener +} from "../src/common.browser/Exports"; +import { + Events, + EventType, + IDetachable, + OCSPEvent, + PlatformEvent +} from "../src/common/Exports"; +import { Settings } from "./Settings"; +import { WaitForPromise } from "./Utilities"; + +import * as fs from "fs"; +import * as os from "os"; +import path from "path"; +import request from "request"; +import rimraf from "rimraf"; + +const origCacehDir: string = process.env.SPEECH_OSCP_CACHE_ROOT; +let cacheDir: string; +let events: OCSPEvent[]; +let currentListener: IDetachable; + +beforeAll(() => { + // override inputs, if necessary + Settings.LoadSettings(); + Events.instance.attachListener(new ConsoleLoggingListener(EventType.Debug)); +}); + +// Test cases are run linerally, the only other mechanism to demark them in the output is to put a console line in each case and +// report the name. +beforeEach(() => { + // tslint:disable-next-line:no-console + console.info("---------------------------------------Starting test case-----------------------------------"); + // tslint:disable-next-line:no-console + console.info("Start Time: " + new Date(Date.now()).toLocaleString()); + cacheDir = path.join(os.tmpdir(), Math.random().toString(36).substr(2, 15)); + process.env.SPEECH_OCSP_CACHE_ROOT = cacheDir; + fs.mkdirSync(cacheDir); + events = []; + currentListener = Events.instance.attach((event: PlatformEvent): void => { + if (event.name.startsWith("OCSP")) { + events.push(event as OCSPEvent); + } + }); + CertCheckAgent.forceReinitDiskCache(); + CertCheckAgent.testTimeOffset = 0; +}); + +afterEach(() => { + // tslint:disable-next-line:no-console + console.info("End Time: " + new Date(Date.now()).toLocaleString()); + rimraf(cacheDir, (error: Error): void => { + // tslint:disable-next-line:no-console + console.info("Error " + Error.toString() + " cleaning up."); + }); + currentListener.detach(); + currentListener = null; +}); + +afterAll(() => { + process.env.SPEECH_OSCP_CACHE_ROOT = origCacehDir; +}); + +function findEvent(eventName: string): number { + let found: number = 0; + events.forEach((event: OCSPEvent, index: number, array: OCSPEvent[]): void => { + if (event.name === eventName) { + found++; + } + }); + + return found; +} + +function waitForEvents(eventName: string, eventCount: number, rejectMessage?: string, timeoutMS: number = 5000): Promise { + return WaitForPromise((): boolean => { + return findEvent(eventName) === eventCount; + }, rejectMessage === undefined ? eventName : rejectMessage, timeoutMS); +} + +function makeRequest(disableOCSPStapling: boolean = true): Promise { + return new Promise((resolve: (value: void) => void, reject: (reason: string) => void): void => { + const testUrl: string = "https://www.microsoft.com/"; + + const agent: CertCheckAgent = new CertCheckAgent(); + + const testRequest: request.Request = request({ + followRedirect: false, + url: testUrl + }, (error: any, response: request.Response, body: any): void => { + if (error !== null) { + reject(error); + } else { + resolve(); + } + + }); + CertCheckAgent.forceDisableOCSPStapling = disableOCSPStapling; + testRequest.agent = agent.GetAgent(); + testRequest.end(); + }); +} + +test("Test OCSP Revoked", (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: Test OCSP Revoked"); + + const testUrl: string = "https://revoked.badssl.com/"; + + const agent: CertCheckAgent = new CertCheckAgent(); + + const testRequest: request.Request = request({ + followRedirect: false, + url: testUrl + }, (error: any, response: request.Response, body: any): void => { + try { + expect(error).not.toBeUndefined(); + expect(error).not.toBeNull(); + expect(error.toString()).toContain("revoked"); + done(); + } catch (ex) { + done.fail(ex); + } + }); + testRequest.agent = agent.GetAgent(); + + testRequest.end(); +}); + +test("Test OCSP Staple", async (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: Test OCSP Staple"); + + await makeRequest(false); + await waitForEvents("OCSPStapleReceivedEvent", 1); + await waitForEvents("OCSPResponseRetrievedEvent", 0); + done(); +}); + +test("Test OCSP Basic", async (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: Test OCSP Basic"); + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + done(); +}); + +test("Test OCSP 2nd request mem cache hit.", async (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: Test OCSP 2nd request mem cache hit."); + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + + events = []; + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 0); + await waitForEvents("OCSPMemoryCacheStoreEvent", 0); + await waitForEvents("OCSPDiskCacheStoreEvent", 0); + await waitForEvents("OCSPDiskCacheHitEvent", 0); + await waitForEvents("OCSPMemoryCacheHitEvent", 1); + + done(); +}); + +test("Test OCSP expirey refreshes.", async (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: Test OCSP expirey refreshes."); + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + + events = []; + CertCheckAgent.testTimeOffset = 1000 * 60 * 60 * 24 * 7.5; + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheHitEvent", 0); + await waitForEvents("OCSPCacheEntryExpiredEvent", 1); + done(); +}); + +test("Test OCSP expirey approaching refreshes.", async (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: Test OCSP expirey approaching refreshes."); + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + + events = []; + CertCheckAgent.testTimeOffset = 1000 * 60 * 60 * 24 * 3.5; + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + await waitForEvents("OCSPCacheUpdateNeededEvent", 1); + await waitForEvents("OCSPCacheUpdatehCompleteEvent", 1); + done(); +}); + +test("Test OCSP invalid cert refreshes.", async (done: jest.DoneCallback) => { + // tslint:disable-next-line:no-console + console.info("Name: invalid cert refreshes."); + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 1); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + + events = []; + CertCheckAgent.forceReinitDiskCache(); + + const dir: string = path.join(cacheDir, "if-you-need-to-delete-this-open-an-issue-async-disk-cache", "microsoft-cognitiveservices-speech-sdk-cache"); + fs.readdir(dir, (error: NodeJS.ErrnoException, files: string[]): void => { + files.forEach((value: string, index: number, array: string[]): void => { + const file: string = path.join(dir, value); + const content: Buffer = fs.readFileSync(file); + content.set([2], 7); + fs.writeFileSync(file, content); + }); + }); + + await makeRequest(); + await waitForEvents("OCSPResponseRetrievedEvent", 1); + await waitForEvents("OCSPMemoryCacheStoreEvent", 2); + await waitForEvents("OCSPDiskCacheStoreEvent", 1); + await waitForEvents("OCSPCacheFetchErrorEvent", 1); + await waitForEvents("OCSPCacheMissEvent", 1); + + done(); + +}); diff --git a/tests/SpeechRecognizerTests.ts b/tests/SpeechRecognizerTests.ts index 5e721ec..5d1e70b 100644 --- a/tests/SpeechRecognizerTests.ts +++ b/tests/SpeechRecognizerTests.ts @@ -17,7 +17,7 @@ import * as request from "request"; import { setTimeout } from "timers"; import { ByteBufferAudioFile } from "./ByteBufferAudioFile"; -import WaitForCondition from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { AudioStreamFormatImpl } from "../src/sdk/Audio/AudioStreamFormat"; diff --git a/tests/SpeechSynthesisTests.ts b/tests/SpeechSynthesisTests.ts index 94acb1b..c89c609 100644 --- a/tests/SpeechSynthesisTests.ts +++ b/tests/SpeechSynthesisTests.ts @@ -11,7 +11,7 @@ import { InvalidOperationError } from "../src/common/Exports"; import { Settings } from "./Settings"; -import WaitForCondition from "./Utilities"; +import { WaitForCondition } from "./Utilities"; let objsToClose: any[]; diff --git a/tests/TranslationRecognizerTests.ts b/tests/TranslationRecognizerTests.ts index f42a723..04a75cc 100644 --- a/tests/TranslationRecognizerTests.ts +++ b/tests/TranslationRecognizerTests.ts @@ -15,7 +15,7 @@ import { import { ByteBufferAudioFile } from "./ByteBufferAudioFile"; import { Settings } from "./Settings"; import { validateTelemetry } from "./TelemetryUtil"; -import { default as WaitForCondition } from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; import { AudioStreamFormatImpl } from "../src/sdk/Audio/AudioStreamFormat"; diff --git a/tests/TranslationSynthTests.ts b/tests/TranslationSynthTests.ts index 0c97c9a..8bec297 100644 --- a/tests/TranslationSynthTests.ts +++ b/tests/TranslationSynthTests.ts @@ -12,7 +12,7 @@ import { import { ByteBufferAudioFile } from "./ByteBufferAudioFile"; import { Settings } from "./Settings"; -import { default as WaitForCondition } from "./Utilities"; +import { WaitForCondition } from "./Utilities"; import { WaveFileAudioInput } from "./WaveFileAudioInputStream"; let objsToClose: any[]; diff --git a/tests/Utilities.ts b/tests/Utilities.ts index 5325d11..527be3c 100644 --- a/tests/Utilities.ts +++ b/tests/Utilities.ts @@ -1,11 +1,31 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -const WaitForCondition = (condition: () => boolean, after: () => void): void => { + +export function WaitForCondition(condition: () => boolean, after: () => void): void { if (condition() === true) { after(); } else { setTimeout(() => WaitForCondition(condition, after), 500); } -}; +} -export default WaitForCondition; +export function sleep(ms: number): Promise { + return new Promise((resolve: (_: void) => void) => setTimeout(resolve, ms)); +} + +export const WaitForPromise = (condition: () => boolean, rejectMessage: string, timeout: number = 60 * 1000): Promise => { + return new Promise(async (resolve: (value: void) => void, reject: (reason: string) => void): Promise => { + const endTime: number = Date.now() + timeout; + + while (!condition() && Date.now() < endTime) { + await sleep(500); + } + + if (Date.now() <= endTime) { + resolve(); + } else { + reject("Condition timeout: " + rejectMessage); + } + }); + +}; diff --git a/tsconfig.json b/tsconfig.json index 46b238b..8875669 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "sourceMap": true, "declaration": true, "noImplicitAny": true, - "removeComments": false + "removeComments": false, + "esModuleInterop": true } } \ No newline at end of file