UNPKG

ac-microsoft-cognitiveservices-speech-sdk

Version:
1 lines 18.3 kB
{"version":3,"sources":["src/common.browser/CertChecks.ts"],"names":[],"mappings":";AAIA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAsB7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAuBxC,qBAAa,cAAc;IAGvB,OAAc,cAAc,EAAE,MAAM,CAAK;IAGzC,OAAc,wBAAwB,EAAE,OAAO,CAAS;IAGxD,OAAO,CAAC,MAAM,CAAC,YAAY,CAAiC;IAG5D,OAAO,CAAC,MAAM,CAAC,aAAa,CAAQ;IAEpC,OAAO,CAAC,aAAa,CAAY;gBAEd,SAAS,CAAC,EAAE,SAAS;WAa1B,oBAAoB,IAAI,IAAI;IAMnC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK;IAetD,OAAO,CAAC,MAAM,CAAC,aAAa;mBAoBP,SAAS;IA0D9B,OAAO,CAAC,MAAM,CAAC,SAAS;mBAkBH,oBAAoB;mBAoEpB,kBAAkB;mBAiClB,WAAW;IAUhC,OAAO,CAAC,MAAM,CAAC,eAAe;IAK9B,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAKpC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAOlC,OAAO,CAAC,MAAM,CAAC,eAAe;IAkC9B,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB,OAAO,CAAC,gBAAgB;CAuC3B","file":"CertChecks.d.ts","sourcesContent":["/* eslint-disable import/order */\r\n// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport * as http from \"http\";\r\nimport * as tls from \"tls\";\r\nimport * as ocsp from \"../../external/ocsp/ocsp\";\r\nimport {\r\n Events,\r\n OCSPCacheEntryExpiredEvent,\r\n OCSPCacheEntryNeedsRefreshEvent,\r\n OCSPCacheFetchErrorEvent,\r\n OCSPCacheHitEvent,\r\n OCSPCacheMissEvent,\r\n OCSPCacheUpdateCompleteEvent,\r\n OCSPCacheUpdateNeededEvent,\r\n OCSPDiskCacheHitEvent,\r\n OCSPDiskCacheStoreEvent,\r\n OCSPEvent,\r\n OCSPMemoryCacheHitEvent,\r\n OCSPMemoryCacheStoreEvent,\r\n OCSPResponseRetrievedEvent,\r\n OCSPStapleReceivedEvent,\r\n OCSPVerificationFailedEvent,\r\n} from \"../common/Exports\";\r\nimport { IStringDictionary } from \"../common/IDictionary\";\r\nimport { ProxyInfo } from \"./ProxyInfo\";\r\n\r\nimport Agent from \"agent-base\";\r\n\r\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\r\n// @ts-ignore\r\nimport Cache from \"async-disk-cache\";\r\nimport HttpsProxyAgent from \"https-proxy-agent\";\r\nimport * as net from \"net\";\r\nimport { OCSPCacheUpdateErrorEvent } from \"../common/OCSPEvents\";\r\n\r\n\r\ninterface tbsUpdateResponse {\r\n thisUpdate: number;\r\n nextUpdate: number;\r\n}\r\n\r\ninterface tbsResponse {\r\n tbsResponseData: {\r\n responses: tbsUpdateResponse[];\r\n };\r\n}\r\n\r\nexport class CertCheckAgent {\r\n\r\n // Test hook to enable forcing expiration / refresh to happen.\r\n public static testTimeOffset: number = 0;\r\n\r\n // Test hook to disable stapling for cache testing.\r\n public static forceDisableOCSPStapling: boolean = false;\r\n\r\n // An in memory cache for recived responses.\r\n private static privMemCache: IStringDictionary<Buffer> = {};\r\n\r\n // The on disk cache.\r\n private static privDiskCache: Cache;\r\n\r\n private privProxyInfo: ProxyInfo;\r\n\r\n public constructor(proxyInfo?: ProxyInfo) {\r\n if (!!proxyInfo) {\r\n this.privProxyInfo = proxyInfo;\r\n }\r\n\r\n // Initialize this here to allow tests to set the env variable before the cache is constructed.\r\n if (!CertCheckAgent.privDiskCache) {\r\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call\r\n 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 });\r\n }\r\n }\r\n\r\n // Test hook to force the disk cache to be recreated.\r\n public static forceReinitDiskCache(): void {\r\n CertCheckAgent.privDiskCache = undefined;\r\n CertCheckAgent.privMemCache = {};\r\n }\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n public GetAgent(disableStapling?: boolean): http.Agent {\r\n // eslint-disable-next-line @typescript-eslint/unbound-method\r\n const agent: any = new Agent.Agent(this.CreateConnection);\r\n\r\n if (this.privProxyInfo !== undefined &&\r\n this.privProxyInfo.HostName !== undefined &&\r\n this.privProxyInfo.Port > 0) {\r\n const proxyName: string = \"privProxyInfo\";\r\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\r\n agent[proxyName] = this.privProxyInfo;\r\n }\r\n\r\n return agent as http.Agent;\r\n }\r\n\r\n private static GetProxyAgent(proxyInfo: ProxyInfo): HttpsProxyAgent {\r\n const httpProxyOptions: HttpsProxyAgent.HttpsProxyAgentOptions = {\r\n host: proxyInfo.HostName,\r\n port: proxyInfo.Port,\r\n };\r\n\r\n if (!!proxyInfo.UserName) {\r\n httpProxyOptions.headers = {\r\n \"Proxy-Authentication\": \"Basic \" + new Buffer(`${proxyInfo.UserName}:${(proxyInfo.Password === undefined) ? \"\" : proxyInfo.Password}`).toString(\"base64\"),\r\n };\r\n } else {\r\n httpProxyOptions.headers = {};\r\n }\r\n\r\n httpProxyOptions.headers.requestOCSP = \"true\";\r\n\r\n const httpProxyAgent: HttpsProxyAgent = new HttpsProxyAgent(httpProxyOptions);\r\n return httpProxyAgent;\r\n }\r\n\r\n private static async OCSPCheck(socketPromise: Promise<net.Socket>, proxyInfo: ProxyInfo): Promise<net.Socket> {\r\n let ocspRequest: ocsp.Request;\r\n let stapling: Buffer;\r\n let resolved: boolean = false;\r\n\r\n const socket: net.Socket = await socketPromise;\r\n socket.cork();\r\n\r\n const tlsSocket: tls.TLSSocket = socket as tls.TLSSocket;\r\n\r\n return new Promise<net.Socket>((resolve: (value: net.Socket) => void, reject: (error: string | Error) => void): void => {\r\n socket.on(\"OCSPResponse\", (data: Buffer): void => {\r\n if (!!data) {\r\n this.onEvent(new OCSPStapleReceivedEvent());\r\n stapling = data;\r\n }\r\n });\r\n\r\n socket.on(\"error\", (error: Error): void => {\r\n if (!resolved) {\r\n resolved = true;\r\n socket.destroy();\r\n reject(error);\r\n }\r\n });\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-misused-promises, @typescript-eslint/explicit-function-return-type\r\n tlsSocket.on(\"secure\", async () => {\r\n const peer: tls.DetailedPeerCertificate = tlsSocket.getPeerCertificate(true);\r\n try {\r\n const issuer: tls.DetailedPeerCertificate = await this.GetIssuer(peer);\r\n\r\n // We always need a request to verify the response.\r\n ocspRequest = ocsp.request.generate(peer.raw, issuer.raw);\r\n\r\n // Do we have a result for this certificate in our memory cache?\r\n const sig: string = ocspRequest.id.toString(\"hex\");\r\n\r\n // Stapled response trumps cached response.\r\n if (!stapling) {\r\n const cacheEntry: Buffer = await CertCheckAgent.GetResponseFromCache(sig, ocspRequest, proxyInfo);\r\n stapling = cacheEntry;\r\n }\r\n\r\n await this.VerifyOCSPResponse(stapling, ocspRequest, proxyInfo);\r\n\r\n socket.uncork();\r\n resolved = true;\r\n resolve(socket);\r\n } catch (e) {\r\n socket.destroy();\r\n resolved = true;\r\n reject(e as string);\r\n }\r\n });\r\n });\r\n }\r\n\r\n private static GetIssuer(peer: tls.DetailedPeerCertificate): Promise<tls.DetailedPeerCertificate> {\r\n if (peer.issuerCertificate) {\r\n return Promise.resolve(peer.issuerCertificate);\r\n }\r\n\r\n return new Promise<tls.DetailedPeerCertificate>((resolve: (value: tls.DetailedPeerCertificate) => void, reject: (reason: string) => void): void => {\r\n const ocspAgent: ocsp.Agent = new ocsp.Agent({});\r\n ocspAgent.fetchIssuer(peer, null, (error: string, value: tls.DetailedPeerCertificate): void => {\r\n if (!!error) {\r\n reject(error);\r\n return;\r\n }\r\n\r\n resolve(value);\r\n });\r\n });\r\n }\r\n\r\n private static async GetResponseFromCache(signature: string, ocspRequest: ocsp.Request, proxyInfo: ProxyInfo): Promise<Buffer> {\r\n let cachedResponse: Buffer = CertCheckAgent.privMemCache[signature];\r\n\r\n if (!!cachedResponse) {\r\n this.onEvent(new OCSPMemoryCacheHitEvent(signature));\r\n }\r\n\r\n // Do we have a result for this certificate on disk in %TMP%?\r\n if (!cachedResponse) {\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call\r\n const diskCacheResponse: { value: Buffer; isCached?: any } = await CertCheckAgent.privDiskCache.get(signature) as { value: Buffer; isCached?: any };\r\n if (!!diskCacheResponse.isCached) {\r\n CertCheckAgent.onEvent(new OCSPDiskCacheHitEvent(signature));\r\n CertCheckAgent.StoreMemoryCacheEntry(signature, diskCacheResponse.value);\r\n cachedResponse = diskCacheResponse.value;\r\n }\r\n } catch (error) {\r\n cachedResponse = null;\r\n }\r\n }\r\n\r\n if (!cachedResponse) {\r\n return cachedResponse;\r\n }\r\n\r\n try {\r\n const cachedOcspResponse: ocsp.Response = ocsp.utils.parseResponse(cachedResponse);\r\n const responseValue: tbsResponse = cachedOcspResponse.value as tbsResponse;\r\n const tbsData: { responses: tbsUpdateResponse[] } = responseValue.tbsResponseData;\r\n if (tbsData.responses.length < 1) {\r\n this.onEvent(new OCSPCacheFetchErrorEvent(signature, \"Not enough data in cached response\"));\r\n return;\r\n }\r\n\r\n const cachedStartTime: number = tbsData.responses[0].thisUpdate;\r\n const cachedNextTime: number = tbsData.responses[0].nextUpdate;\r\n\r\n if (cachedNextTime < (Date.now() + this.testTimeOffset - 60000)) {\r\n // Cached entry has expired.\r\n this.onEvent(new OCSPCacheEntryExpiredEvent(signature, cachedNextTime));\r\n cachedResponse = null;\r\n } else {\r\n // If we're within one day of the next update, or 50% of the way through the validity period,\r\n // background an update to the cache.\r\n\r\n const minUpdate: number = Math.min(24 * 60 * 60 * 1000, (cachedNextTime - cachedStartTime) / 2);\r\n\r\n if ((cachedNextTime - (Date.now() + this.testTimeOffset)) < minUpdate) {\r\n this.onEvent(new OCSPCacheEntryNeedsRefreshEvent(signature, cachedStartTime, cachedNextTime));\r\n this.UpdateCache(ocspRequest, proxyInfo).catch((error: string): void => {\r\n // Well, not much we can do here.\r\n this.onEvent(new OCSPCacheUpdateErrorEvent(signature, error.toString()));\r\n });\r\n } else {\r\n this.onEvent(new OCSPCacheHitEvent(signature, cachedStartTime, cachedNextTime));\r\n }\r\n }\r\n } catch (error) {\r\n this.onEvent(new OCSPCacheFetchErrorEvent(signature, error as string));\r\n cachedResponse = null;\r\n }\r\n if (!cachedResponse) {\r\n this.onEvent(new OCSPCacheMissEvent(signature));\r\n }\r\n return cachedResponse;\r\n }\r\n\r\n private static async VerifyOCSPResponse(cacheValue: Buffer, ocspRequest: ocsp.Request, proxyInfo: ProxyInfo): Promise<void> {\r\n let ocspResponse: Buffer = cacheValue;\r\n\r\n // Do we have a valid response?\r\n if (!ocspResponse) {\r\n ocspResponse = await CertCheckAgent.GetOCSPResponse(ocspRequest, proxyInfo);\r\n }\r\n\r\n return new Promise<void>((resolve: () => void, reject: (error: string | Error) => void): void => {\r\n ocsp.verify({ request: ocspRequest, response: ocspResponse }, (error: string): void => {\r\n if (!!error) {\r\n CertCheckAgent.onEvent(new OCSPVerificationFailedEvent(ocspRequest.id.toString(\"hex\"), error));\r\n\r\n // Bad Cached Value? One more try without the cache.\r\n if (!!cacheValue) {\r\n this.VerifyOCSPResponse(null, ocspRequest, proxyInfo).then((): void => {\r\n resolve();\r\n }, (error: Error): void => {\r\n reject(error);\r\n });\r\n } else {\r\n reject(error);\r\n }\r\n } else {\r\n if (!cacheValue) {\r\n CertCheckAgent.StoreCacheEntry(ocspRequest.id.toString(\"hex\"), ocspResponse);\r\n }\r\n resolve();\r\n }\r\n });\r\n });\r\n }\r\n\r\n private static async UpdateCache(req: ocsp.Request, proxyInfo: ProxyInfo): Promise<void> {\r\n const signature: string = req.id.toString(\"hex\");\r\n this.onEvent(new OCSPCacheUpdateNeededEvent(signature));\r\n\r\n const rawResponse: Buffer = await this.GetOCSPResponse(req, proxyInfo);\r\n this.StoreCacheEntry(signature, rawResponse);\r\n this.onEvent(new OCSPCacheUpdateCompleteEvent(req.id.toString(\"hex\")));\r\n\r\n }\r\n\r\n private static StoreCacheEntry(sig: string, rawResponse: Buffer): void {\r\n this.StoreMemoryCacheEntry(sig, rawResponse);\r\n this.StoreDiskCacheEntry(sig, rawResponse);\r\n }\r\n\r\n private static StoreMemoryCacheEntry(sig: string, rawResponse: Buffer): void {\r\n this.privMemCache[sig] = rawResponse;\r\n this.onEvent(new OCSPMemoryCacheStoreEvent(sig));\r\n }\r\n\r\n private static StoreDiskCacheEntry(sig: string, rawResponse: Buffer): void {\r\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call\r\n this.privDiskCache.set(sig, rawResponse).then((): void => {\r\n this.onEvent(new OCSPDiskCacheStoreEvent(sig));\r\n });\r\n }\r\n\r\n private static GetOCSPResponse(req: ocsp.Request, proxyInfo: ProxyInfo): Promise<Buffer> {\r\n\r\n const ocspMethod: string = \"1.3.6.1.5.5.7.48.1\";\r\n let options: http.RequestOptions = {};\r\n\r\n if (!!proxyInfo) {\r\n const agent: HttpsProxyAgent = CertCheckAgent.GetProxyAgent(proxyInfo);\r\n options.agent = agent;\r\n }\r\n\r\n return new Promise<Buffer>((resolve: (value: Buffer) => void, reject: (error: string | Error) => void): void => {\r\n ocsp.utils.getAuthorityInfo(req.cert as tls.DetailedPeerCertificate, ocspMethod, (error: string, uri: string): void => {\r\n if (error) {\r\n reject(error);\r\n return;\r\n }\r\n\r\n const url = new URL(uri);\r\n options = { ...options, host: url.host, protocol: url.protocol, port: url.port, path: url.pathname, hostname: url.host };\r\n\r\n ocsp.utils.getResponse(options, req.data, (error: string, raw: Buffer): void => {\r\n if (error) {\r\n reject(error);\r\n return;\r\n }\r\n\r\n const certID: Buffer = req.certID as Buffer;\r\n this.onEvent(new OCSPResponseRetrievedEvent(certID.toString(\"hex\")));\r\n resolve(raw);\r\n });\r\n });\r\n });\r\n }\r\n\r\n private static onEvent(event: OCSPEvent): void {\r\n Events.instance.onEvent(event);\r\n }\r\n\r\n private CreateConnection(request: Agent.ClientRequest, options: Agent.RequestOptions): Promise<net.Socket> {\r\n const enableOCSP: boolean = (typeof process !== \"undefined\" && process.env.NODE_TLS_REJECT_UNAUTHORIZED !== \"0\" && process.env.SPEECH_CONDUCT_OCSP_CHECK !== \"0\") && options.secureEndpoint;\r\n let socketPromise: Promise<net.Socket>;\r\n\r\n options = {\r\n ...options,\r\n ...{\r\n requestOCSP: !CertCheckAgent.forceDisableOCSPStapling,\r\n servername: options.host\r\n }\r\n };\r\n\r\n if (!!this.privProxyInfo) {\r\n const httpProxyAgent: HttpsProxyAgent = CertCheckAgent.GetProxyAgent(this.privProxyInfo);\r\n const baseAgent: Agent.Agent = httpProxyAgent as unknown as Agent.Agent;\r\n\r\n socketPromise = new Promise<net.Socket>((resolve: (value: net.Socket) => void, reject: (error: string | Error) => void): void => {\r\n baseAgent.callback(request, options, (error: Error, socket: net.Socket): void => {\r\n if (!!error) {\r\n reject(error);\r\n } else {\r\n resolve(socket);\r\n }\r\n });\r\n });\r\n } else {\r\n if (!!options.secureEndpoint) {\r\n socketPromise = Promise.resolve(tls.connect(options));\r\n } else {\r\n socketPromise = Promise.resolve(net.connect(options));\r\n }\r\n }\r\n\r\n if (!!enableOCSP) {\r\n return CertCheckAgent.OCSPCheck(socketPromise, this.privProxyInfo);\r\n } else {\r\n return socketPromise;\r\n }\r\n }\r\n}\r\n"]}