{"version":3,"sources":["src/common.browser/WebsocketMessageAdapter.ts"],"names":[],"mappings":"AAaA,OAAO,EAMH,eAAe,EACf,iBAAiB,EAGjB,sBAAsB,EAGtB,eAAe,EAGf,WAAW,EACX,0BAA0B,EAI7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAQ3C,qBAAa,uBAAuB;IAChC,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,oBAAoB,CAA6B;IACzD,OAAO,CAAC,mBAAmB,CAAiB;IAE5C,OAAO,CAAC,oBAAoB,CAAmB;IAC/C,OAAO,CAAC,yBAAyB,CAA2B;IAC5D,OAAO,CAAC,+BAA+B,CAAmC;IAC1E,OAAO,CAAC,gCAAgC,CAAiB;IACzD,OAAO,CAAC,sBAAsB,CAAiB;IAC/C,OAAO,CAAC,oBAAoB,CAA+B;IAC3D,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,qBAAqB,CAAU;IAEvC,OAAc,iBAAiB,EAAE,OAAO,CAAS;gBAG7C,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,0BAA0B,EAC5C,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,EAClC,iBAAiB,EAAE,OAAO;IA0B9B,IAAW,KAAK,IAAI,eAAe,CAElC;IAEM,IAAI,IAAI,OAAO,CAAC,sBAAsB,CAAC;IAkIvC,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB/C,IAAI,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAQlC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5C,IAAW,MAAM,IAAI,WAAW,CAAC,eAAe,CAAC,CAEhD;IAED,OAAO,CAAC,cAAc;YAuBR,OAAO;YAcP,gBAAgB;IAkB9B,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,MAAM,CAAC,aAAa;IAoB5B,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,KAAK,eAAe,GAE1B;CAEJ","file":"WebsocketMessageAdapter.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\n// Node.JS specific web socket / browser support.\r\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\r\nimport * as http from \"http\";\r\nimport * as net from \"net\";\r\nimport * as tls from \"tls\";\r\nimport Agent from \"agent-base\";\r\nimport HttpsProxyAgent from \"https-proxy-agent\";\r\n\r\nimport ws from \"ws\";\r\nimport { HeaderNames } from \"../common.speech/HeaderNames.js\";\r\nimport {\r\n    ArgumentNullError,\r\n    BackgroundEvent,\r\n    ConnectionClosedEvent,\r\n    ConnectionErrorEvent,\r\n    ConnectionEstablishedEvent,\r\n    ConnectionEvent,\r\n    ConnectionMessage,\r\n    ConnectionMessageReceivedEvent,\r\n    ConnectionMessageSentEvent,\r\n    ConnectionOpenResponse,\r\n    ConnectionRedirectEvent,\r\n    ConnectionStartEvent,\r\n    ConnectionState,\r\n    Deferred,\r\n    Events,\r\n    EventSource,\r\n    IWebsocketMessageFormatter,\r\n    MessageType,\r\n    Queue,\r\n    RawWebsocketMessage,\r\n} from \"../common/Exports.js\";\r\nimport { ProxyInfo } from \"./ProxyInfo.js\";\r\n\r\ninterface ISendItem {\r\n    Message: ConnectionMessage;\r\n    RawWebsocketMessage: RawWebsocketMessage;\r\n    sendStatusDeferral: Deferred<void>;\r\n}\r\n\r\nexport class WebsocketMessageAdapter {\r\n    private privConnectionState: ConnectionState;\r\n    private privMessageFormatter: IWebsocketMessageFormatter;\r\n    private privWebsocketClient: WebSocket | ws;\r\n\r\n    private privSendMessageQueue: Queue<ISendItem>;\r\n    private privReceivingMessageQueue: Queue<ConnectionMessage>;\r\n    private privConnectionEstablishDeferral: Deferred<ConnectionOpenResponse>;\r\n    private privCertificateValidatedDeferral: Deferred<void>;\r\n    private privDisconnectDeferral: Deferred<void>;\r\n    private privConnectionEvents: EventSource<ConnectionEvent>;\r\n    private privConnectionId: string;\r\n    private privUri: string;\r\n    private proxyInfo: ProxyInfo;\r\n    private privHeaders: { [key: string]: string };\r\n    private privLastErrorReceived: string;\r\n    private privEnableCompression: boolean;\r\n\r\n    public static forceNpmWebSocket: boolean = false;\r\n\r\n    public constructor(\r\n        uri: string,\r\n        connectionId: string,\r\n        messageFormatter: IWebsocketMessageFormatter,\r\n        proxyInfo: ProxyInfo,\r\n        headers: { [key: string]: string },\r\n        enableCompression: boolean) {\r\n\r\n        if (!uri) {\r\n            throw new ArgumentNullError(\"uri\");\r\n        }\r\n\r\n        if (!messageFormatter) {\r\n            throw new ArgumentNullError(\"messageFormatter\");\r\n        }\r\n\r\n        this.proxyInfo = proxyInfo;\r\n        this.privConnectionEvents = new EventSource<ConnectionEvent>();\r\n        this.privConnectionId = connectionId;\r\n        this.privMessageFormatter = messageFormatter;\r\n        this.privConnectionState = ConnectionState.None;\r\n        this.privUri = uri;\r\n        this.privHeaders = headers;\r\n        this.privEnableCompression = enableCompression;\r\n\r\n        // Add the connection ID to the headers\r\n        this.privHeaders[HeaderNames.ConnectionId] = this.privConnectionId;\r\n        this.privHeaders.connectionId = this.privConnectionId;\r\n\r\n        this.privLastErrorReceived = \"\";\r\n    }\r\n\r\n    public get state(): ConnectionState {\r\n        return this.privConnectionState;\r\n    }\r\n\r\n    public open(): Promise<ConnectionOpenResponse> {\r\n        if (this.privConnectionState === ConnectionState.Disconnected) {\r\n            return Promise.reject<ConnectionOpenResponse>(`Cannot open a connection that is in ${this.privConnectionState} state`);\r\n        }\r\n\r\n        if (this.privConnectionEstablishDeferral) {\r\n            return this.privConnectionEstablishDeferral.promise;\r\n        }\r\n\r\n        this.privConnectionEstablishDeferral = new Deferred<ConnectionOpenResponse>();\r\n        this.privCertificateValidatedDeferral = new Deferred<void>();\r\n\r\n        this.privConnectionState = ConnectionState.Connecting;\r\n\r\n        try {\r\n\r\n            const proxyConfiguredInNode: boolean = typeof window === \"undefined\" && !!this.proxyInfo?.HostName;\r\n\r\n            if (typeof WebSocket !== \"undefined\" && !WebsocketMessageAdapter.forceNpmWebSocket && !proxyConfiguredInNode) {\r\n                // Browser handles cert checks.\r\n                this.privCertificateValidatedDeferral.resolve();\r\n\r\n                this.privWebsocketClient = new WebSocket(this.privUri);\r\n            } else {\r\n                // Workaround for https://github.com/microsoft/cognitive-services-speech-sdk-js/issues/465\r\n                // Which is root caused by https://github.com/TooTallNate/node-agent-base/issues/61\r\n                const uri = new URL(this.privUri);\r\n                let protocol: string = uri.protocol;\r\n\r\n                if (protocol?.toLocaleLowerCase() === \"wss:\") {\r\n                    protocol = \"https:\";\r\n                } else if (protocol?.toLocaleLowerCase() === \"ws:\") {\r\n                    protocol = \"http:\";\r\n                }\r\n\r\n                const options: ws.ClientOptions = { headers: this.privHeaders, perMessageDeflate: this.privEnableCompression, followRedirects: protocol.toLocaleLowerCase() === \"https:\" };\r\n                // The ocsp library will handle validation for us and fail the connection if needed.\r\n                this.privCertificateValidatedDeferral.resolve();\r\n\r\n                options.agent = this.getAgent();\r\n\r\n                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\r\n                (options.agent as any).protocol = protocol;\r\n                this.privWebsocketClient = new ws(this.privUri, options);\r\n                this.privWebsocketClient.on(\"redirect\", (redirectUrl: string): void => {\r\n                    const event: ConnectionRedirectEvent = new ConnectionRedirectEvent(this.privConnectionId, redirectUrl, this.privUri, `Getting redirect URL from endpoint ${this.privUri} with redirect URL '${redirectUrl}'`);\r\n                    Events.instance.onEvent(event);\r\n                });\r\n            }\r\n\r\n            this.privWebsocketClient.binaryType = \"arraybuffer\";\r\n            this.privReceivingMessageQueue = new Queue<ConnectionMessage>();\r\n            this.privDisconnectDeferral = new Deferred<void>();\r\n            this.privSendMessageQueue = new Queue<ISendItem>();\r\n            this.processSendQueue().catch((reason: string): void => {\r\n                Events.instance.onEvent(new BackgroundEvent(reason));\r\n            });\r\n        } catch (error) {\r\n            this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(500, error as string));\r\n            return this.privConnectionEstablishDeferral.promise;\r\n        }\r\n\r\n        this.onEvent(new ConnectionStartEvent(this.privConnectionId, this.privUri));\r\n\r\n        this.privWebsocketClient.onopen = (): void => {\r\n            this.privCertificateValidatedDeferral.promise.then((): void => {\r\n                this.privConnectionState = ConnectionState.Connected;\r\n                this.onEvent(new ConnectionEstablishedEvent(this.privConnectionId));\r\n                this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(200, \"\"));\r\n            }, (error: string): void => {\r\n                this.privConnectionEstablishDeferral.reject(error);\r\n            });\r\n        };\r\n\r\n        this.privWebsocketClient.onerror = (e: { error: any; message: string; type: string; target: WebSocket | ws }): void => {\r\n            this.onEvent(new ConnectionErrorEvent(this.privConnectionId, e.message, e.type));\r\n            this.privLastErrorReceived = e.message;\r\n        };\r\n\r\n        this.privWebsocketClient.onclose = (e: { wasClean: boolean; code: number; reason: string; target: WebSocket | ws }): void => {\r\n            if (this.privConnectionState === ConnectionState.Connecting) {\r\n                this.privConnectionState = ConnectionState.Disconnected;\r\n                // this.onEvent(new ConnectionEstablishErrorEvent(this.connectionId, e.code, e.reason));\r\n                this.privConnectionEstablishDeferral.resolve(new ConnectionOpenResponse(e.code, e.reason + \" \" + this.privLastErrorReceived));\r\n            } else {\r\n                this.privConnectionState = ConnectionState.Disconnected;\r\n                this.privWebsocketClient = null;\r\n                this.onEvent(new ConnectionClosedEvent(this.privConnectionId, e.code, e.reason));\r\n            }\r\n\r\n            this.onClose(e.code, e.reason).catch((reason: string): void => {\r\n                Events.instance.onEvent(new BackgroundEvent(reason));\r\n            });\r\n        };\r\n\r\n        this.privWebsocketClient.onmessage = (e: { data: ws.Data; type: string; target: WebSocket | ws }): void => {\r\n            const networkReceivedTime = new Date().toISOString();\r\n            if (this.privConnectionState === ConnectionState.Connected) {\r\n                const deferred = new Deferred<ConnectionMessage>();\r\n                // let id = ++this.idCounter;\r\n                this.privReceivingMessageQueue.enqueueFromPromise(deferred.promise);\r\n                if (e.data instanceof ArrayBuffer) {\r\n                    const rawMessage = new RawWebsocketMessage(MessageType.Binary, e.data);\r\n                    this.privMessageFormatter\r\n                        .toConnectionMessage(rawMessage)\r\n                        .then((connectionMessage: ConnectionMessage): void => {\r\n                            this.onEvent(new ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));\r\n                            deferred.resolve(connectionMessage);\r\n                        }, (error: string): void => {\r\n                            // TODO: Events for these ?\r\n                            deferred.reject(`Invalid binary message format. Error: ${error}`);\r\n                        });\r\n                } else {\r\n                    const rawMessage = new RawWebsocketMessage(MessageType.Text, e.data);\r\n                    this.privMessageFormatter\r\n                        .toConnectionMessage(rawMessage)\r\n                        .then((connectionMessage: ConnectionMessage): void => {\r\n                            this.onEvent(new ConnectionMessageReceivedEvent(this.privConnectionId, networkReceivedTime, connectionMessage));\r\n                            deferred.resolve(connectionMessage);\r\n                        }, (error: string): void => {\r\n                            // TODO: Events for these ?\r\n                            deferred.reject(`Invalid text message format. Error: ${error}`);\r\n                        });\r\n                }\r\n            }\r\n        };\r\n\r\n        return this.privConnectionEstablishDeferral.promise;\r\n    }\r\n\r\n    public send(message: ConnectionMessage): Promise<void> {\r\n        if (this.privConnectionState !== ConnectionState.Connected) {\r\n            return Promise.reject(`Cannot send on connection that is in ${ConnectionState[this.privConnectionState]} state`);\r\n        }\r\n\r\n        const messageSendStatusDeferral = new Deferred<void>();\r\n        const messageSendDeferral = new Deferred<ISendItem>();\r\n\r\n        this.privSendMessageQueue.enqueueFromPromise(messageSendDeferral.promise);\r\n\r\n        this.privMessageFormatter\r\n            .fromConnectionMessage(message)\r\n            .then((rawMessage: RawWebsocketMessage): void => {\r\n                messageSendDeferral.resolve({\r\n                    Message: message,\r\n                    RawWebsocketMessage: rawMessage,\r\n                    sendStatusDeferral: messageSendStatusDeferral,\r\n                });\r\n            }, (error: string): void => {\r\n                messageSendDeferral.reject(`Error formatting the message. ${error}`);\r\n            });\r\n\r\n        return messageSendStatusDeferral.promise;\r\n    }\r\n\r\n    public read(): Promise<ConnectionMessage> {\r\n        if (this.privConnectionState !== ConnectionState.Connected) {\r\n            return Promise.reject<ConnectionMessage>(`Cannot read on connection that is in ${this.privConnectionState} state`);\r\n        }\r\n\r\n        return this.privReceivingMessageQueue.dequeue();\r\n    }\r\n\r\n    public close(reason?: string): Promise<void> {\r\n        if (this.privWebsocketClient) {\r\n            if (this.privConnectionState !== ConnectionState.Disconnected) {\r\n                this.privWebsocketClient.close(1000, reason ? reason : \"Normal closure by client\");\r\n            }\r\n        } else {\r\n            return Promise.resolve();\r\n        }\r\n\r\n        return this.privDisconnectDeferral.promise;\r\n    }\r\n\r\n    public get events(): EventSource<ConnectionEvent> {\r\n        return this.privConnectionEvents;\r\n    }\r\n\r\n    private sendRawMessage(sendItem: ISendItem): Promise<void> {\r\n        try {\r\n            // indicates we are draining the queue and it came with no message;\r\n            if (!sendItem) {\r\n                return Promise.resolve();\r\n            }\r\n\r\n            this.onEvent(new ConnectionMessageSentEvent(this.privConnectionId, new Date().toISOString(), sendItem.Message));\r\n\r\n            // add a check for the ws readystate in order to stop the red console error 'WebSocket is already in CLOSING or CLOSED state' appearing\r\n            if (this.isWebsocketOpen) {\r\n                // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\r\n                this.privWebsocketClient.send(sendItem.RawWebsocketMessage.payload);\r\n            } else {\r\n                return Promise.reject(\"websocket send error: Websocket not ready \" + this.privConnectionId + \" \" + sendItem.Message.id + \" \" + new Error().stack);\r\n            }\r\n            return Promise.resolve();\r\n\r\n        } catch (e) {\r\n            return Promise.reject(`websocket send error: ${e as string}`);\r\n        }\r\n    }\r\n\r\n    private async onClose(code: number, reason: string): Promise<void> {\r\n        const closeReason = `Connection closed. ${code}: ${reason}`;\r\n        this.privConnectionState = ConnectionState.Disconnected;\r\n        this.privDisconnectDeferral.resolve();\r\n        await this.privReceivingMessageQueue.drainAndDispose((): void => {\r\n            // TODO: Events for these ?\r\n            // Logger.instance.onEvent(new LoggingEvent(LogType.Warning, null, `Failed to process received message. Reason: ${closeReason}, Message: ${JSON.stringify(pendingReceiveItem)}`));\r\n        }, closeReason);\r\n\r\n        await this.privSendMessageQueue.drainAndDispose((pendingSendItem: ISendItem): void => {\r\n            pendingSendItem.sendStatusDeferral.reject(closeReason);\r\n        }, closeReason);\r\n    }\r\n\r\n    private async processSendQueue(): Promise<void> {\r\n        while (true) {\r\n            const itemToSend: Promise<ISendItem> = this.privSendMessageQueue.dequeue();\r\n            const sendItem: ISendItem = await itemToSend;\r\n            // indicates we are draining the queue and it came with no message;\r\n            if (!sendItem) {\r\n                return;\r\n            }\r\n\r\n            try {\r\n                await this.sendRawMessage(sendItem);\r\n                sendItem.sendStatusDeferral.resolve();\r\n            } catch (sendError) {\r\n                sendItem.sendStatusDeferral.reject(sendError as string);\r\n            }\r\n        }\r\n    }\r\n\r\n    private onEvent(event: ConnectionEvent): void {\r\n        this.privConnectionEvents.onEvent(event);\r\n        Events.instance.onEvent(event);\r\n    }\r\n\r\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n    private getAgent(): http.Agent {\r\n        // eslint-disable-next-line @typescript-eslint/unbound-method\r\n        const agent: { proxyInfo: ProxyInfo } = new Agent.Agent(this.createConnection) as unknown as { proxyInfo: ProxyInfo };\r\n\r\n        if (this.proxyInfo !== undefined &&\r\n            this.proxyInfo.HostName !== undefined &&\r\n            this.proxyInfo.Port > 0) {\r\n            agent.proxyInfo = this.proxyInfo;\r\n        }\r\n\r\n        return agent as unknown 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 \" + Buffer.from(`${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 createConnection(request: Agent.ClientRequest, options: Agent.RequestOptions): Promise<net.Socket> {\r\n        let socketPromise: Promise<net.Socket>;\r\n\r\n        options = {\r\n            ...options,\r\n            ...{\r\n                requestOCSP: true,\r\n                servername: options.host\r\n            }\r\n        };\r\n\r\n        if (!!this.proxyInfo) {\r\n            const httpProxyAgent: HttpsProxyAgent = WebsocketMessageAdapter.GetProxyAgent(this.proxyInfo);\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        return socketPromise;\r\n    }\r\n\r\n    private get isWebsocketOpen(): boolean {\r\n        return this.privWebsocketClient && this.privWebsocketClient.readyState === this.privWebsocketClient.OPEN;\r\n    }\r\n\r\n}\r\n"]}