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