UNPKG

react-native

Version:

A framework for building native apps using React

276 lines (248 loc) • 8.92 kB
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow strict-local */ import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import Platform from '../Utilities/Platform'; import NativeWebSocketModule from './NativeWebSocketModule'; import base64 from 'base64-js'; const originalRCTWebSocketConnect = NativeWebSocketModule.connect; const originalRCTWebSocketSend = NativeWebSocketModule.send; const originalRCTWebSocketSendBinary = NativeWebSocketModule.sendBinary; const originalRCTWebSocketClose = NativeWebSocketModule.close; let eventEmitter; let subscriptions; let closeCallback; let sendCallback; let connectCallback; let onOpenCallback; let onMessageCallback; let onErrorCallback; let onCloseCallback; let isInterceptorEnabled = false; /** * A network interceptor which monkey-patches RCTWebSocketModule methods * to gather all websocket network requests/responses, in order to show * their information in the React Native inspector development tool. */ const WebSocketInterceptor = { /** * Invoked when RCTWebSocketModule.close(...) is called. * The callback is invoked with the following parameters: * - `'code'` - The status code explaining why the connection was closed, * - `'reason'` - The reason explaining why the connection was closed, * - `'socketId'` - The id of the socket. */ setCloseCallback(callback: ?(number | null, string | null, number) => void) { closeCallback = callback; }, /** * Invoked when RCTWebSocketModule.send(...) or sendBinary(...) is called. * The callback is invoked with the following parameters: * - `'data'` - The data sent, * - `'socketId'` - The id of the socket. */ setSendCallback(callback: ?(string | ArrayBuffer, number) => void) { sendCallback = callback; }, /** * Invoked when RCTWebSocketModule.connect(...) is called. * The callback is invoked with the following parameters: * - `'url'` - The url of the socket, * - `'protocols'` - The protocols of the socket, * - `'options'` - The options of the socket, * - `'socketId'` - The id of the socket. */ setConnectCallback( callback: ?(string, Array<string> | null, Array<string>, number) => void, ) { connectCallback = callback; }, /** * Invoked when event "websocketOpen" happens. * The callback is invoked with the following parameters: * - `'socketId'` - The id of the socket. */ setOnOpenCallback(callback: ?(number) => void) { onOpenCallback = callback; }, /** * Invoked when event "websocketMessage" happens. * The callback is invoked with the following parameters: * - `'data'` - The data received, * - `'socketId'` - The id of the socket. */ setOnMessageCallback(callback: ?(string | ArrayBuffer, number) => void) { onMessageCallback = callback; }, /** * Invoked when event "websocketFailed" happens. * The callback is invoked with the following parameters: * - `'message'` - The error message, * - `'socketId'` - The id of the socket. */ setOnErrorCallback(callback: ?({message: string}, number) => void) { onErrorCallback = callback; }, /** * Invoked when event "websocketClosed" happens. * The callback is invoked with the following parameters: * - `'code'` - The status code explaining why the connection was closed, * - `'reason'` - The reason explaining why the connection was closed, * - `'socketId'` - The id of the socket. */ setOnCloseCallback( callback: ?({code: number, reason: ?string}, number) => void, ) { onCloseCallback = callback; }, isInterceptorEnabled(): boolean { return isInterceptorEnabled; }, _unregisterEvents() { subscriptions.forEach(e => e.remove()); subscriptions = []; }, /** * Add listeners to the RCTWebSocketModule events to intercept them. */ _registerEvents() { subscriptions = [ // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketMessage', ev => { if (onMessageCallback) { onMessageCallback( ev.type === 'binary' ? WebSocketInterceptor._arrayBufferToString(ev.data) : ev.data, ev.id, ); } }), // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketOpen', ev => { if (onOpenCallback) { onOpenCallback(ev.id); } }), // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketClosed', ev => { if (onCloseCallback) { onCloseCallback({code: ev.code, reason: ev.reason}, ev.id); } }), // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketFailed', ev => { if (onErrorCallback) { onErrorCallback({message: ev.message}, ev.id); } }), ]; }, enableInterception() { if (isInterceptorEnabled) { return; } // $FlowFixMe[underconstrained-implicit-instantiation] eventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior Platform.OS !== 'ios' ? null : NativeWebSocketModule, ); WebSocketInterceptor._registerEvents(); // Override `connect` method for all RCTWebSocketModule requests // to intercept the request url, protocols, options and socketId, // then pass them through the `connectCallback`. // $FlowFixMe[cannot-write] // $FlowFixMe[missing-this-annot] NativeWebSocketModule.connect = function ( url: string, protocols: Array<string> | null, options: $FlowFixMe, socketId: number, ) { if (connectCallback) { connectCallback(url, protocols, options, socketId); } originalRCTWebSocketConnect.apply(this, arguments); }; // Override `send` method for all RCTWebSocketModule requests to intercept // the data sent, then pass them through the `sendCallback`. // $FlowFixMe[cannot-write] // $FlowFixMe[missing-this-annot] NativeWebSocketModule.send = function (data, socketId) { if (sendCallback) { sendCallback(data, socketId); } originalRCTWebSocketSend.apply(this, arguments); }; // Override `sendBinary` method for all RCTWebSocketModule requests to // intercept the data sent, then pass them through the `sendCallback`. // $FlowFixMe[cannot-write] // $FlowFixMe[missing-this-annot] NativeWebSocketModule.sendBinary = function (data, socketId) { if (sendCallback) { sendCallback(WebSocketInterceptor._arrayBufferToString(data), socketId); } originalRCTWebSocketSendBinary.apply(this, arguments); }; // Override `close` method for all RCTWebSocketModule requests to intercept // the close information, then pass them through the `closeCallback`. // $FlowFixMe[cannot-write] // $FlowFixMe[missing-this-annot] NativeWebSocketModule.close = function () { if (closeCallback) { if (arguments.length === 3) { closeCallback(arguments[0], arguments[1], arguments[2]); } else { closeCallback(null, null, arguments[0]); } } originalRCTWebSocketClose.apply(this, arguments); }; isInterceptorEnabled = true; }, _arrayBufferToString(data: string): ArrayBuffer | string { const value = base64.toByteArray(data).buffer; if (value === undefined || value === null) { return '(no value)'; } if ( typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined' && value instanceof ArrayBuffer ) { return `ArrayBuffer {${String(Array.from(new Uint8Array(value)))}}`; } return value; }, // Unpatch RCTWebSocketModule methods and remove the callbacks. disableInterception() { if (!isInterceptorEnabled) { return; } isInterceptorEnabled = false; // $FlowFixMe[cannot-write] NativeWebSocketModule.send = originalRCTWebSocketSend; // $FlowFixMe[cannot-write] NativeWebSocketModule.sendBinary = originalRCTWebSocketSendBinary; // $FlowFixMe[cannot-write] NativeWebSocketModule.close = originalRCTWebSocketClose; // $FlowFixMe[cannot-write] NativeWebSocketModule.connect = originalRCTWebSocketConnect; connectCallback = null; closeCallback = null; sendCallback = null; onOpenCallback = null; onMessageCallback = null; onCloseCallback = null; onErrorCallback = null; WebSocketInterceptor._unregisterEvents(); }, }; export default WebSocketInterceptor;