diff --git a/package.json b/package.json index 638c4980e..c8605f70d 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@types/uuid": "^3.4.5", "@typescript-eslint/eslint-plugin": "^2.19.2", "@typescript-eslint/parser": "^2.19.2", + "@types/ws": "^7.2.0", "babel-code-frame": "^6.26.0", "babel-eslint": "^10.0.1", "electron": "7.1.2", diff --git a/src/server.tsx b/src/server.tsx index 28bd9779f..39e9238c3 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -14,7 +14,6 @@ import {Store} from './reducers/index'; import CertificateProvider from './utils/CertificateProvider'; import {RSocketServer} from 'rsocket-core'; import RSocketTCPServer from 'rsocket-tcp-server'; -import {Single} from 'rsocket-flowable'; import Client from './Client'; import {FlipperClientConnection} from './Client'; import {UninitializedClient} from './UninitializedClient'; @@ -27,6 +26,13 @@ import {Responder, Payload, ReactiveSocket} from 'rsocket-types'; import GK from './fb-stubs/GK'; import {initJsEmulatorIPC} from './utils/js-client/serverUtils'; import {buildClientId} from './utils/clientUtils'; +import {Single} from 'rsocket-flowable'; +import WebSocket from 'ws'; +import JSDevice from './devices/JSDevice'; +import {WebsocketClientFlipperConnection} from './utils/js-client/websocketClientFlipperConnection'; +import querystring from 'querystring'; +import {IncomingMessage} from 'http'; +const ws = window.require('ws'); // Electron tries to get you to use browser's ws instead, so can't use import. type ClientInfo = { connection: FlipperClientConnection | null | undefined; @@ -86,6 +92,9 @@ class Server extends EventEmitter { this.insecureServer = this.startServer(insecure); return; }); + if (GK.get('comet_enable_flipper_connection')) { + this.startWsServer(8333); + } reportPlatformFailures(this.initialisePromise, 'initializeServer'); if (GK.get('flipper_js_client_emulator')) { @@ -139,6 +148,90 @@ class Server extends EventEmitter { }); } + startWsServer(port: number) { + const wss = new ws.Server({ + host: 'localhost', + port, + verifyClient: (info: { + origin: string; + req: IncomingMessage; + secure: boolean; + }) => { + return info.origin.startsWith('chrome-extension://'); + }, + }); + wss.on('connection', (ws: WebSocket, message: any) => { + const clients: {[app: string]: Promise} = {}; + const query = querystring.decode(message.url.split('?')[1]); + const deviceId: string = + typeof query.deviceId === 'string' ? query.deviceId : 'webbrowser'; + this.store.dispatch({ + type: 'REGISTER_DEVICE', + payload: new JSDevice(deviceId, 'Web Browser', 1), + }); + + const cleanup = () => { + Object.values(clients).map(p => + p.then(c => this.removeConnection(c.id)), + ); + this.store.dispatch({ + type: 'UNREGISTER_DEVICES', + payload: new Set([deviceId]), + }); + }; + + ws.on('message', (rawMessage: any) => { + const message = JSON.parse(rawMessage.toString()); + switch (message.type) { + case 'connect': { + const app = message.app; + const plugins = message.plugins; + const client = this.addConnection( + new WebsocketClientFlipperConnection(ws, app, plugins), + { + app, + os: 'JSWebApp', + device: 'device', + device_id: deviceId, + sdk_version: 1, + }, + {}, + ); + clients[app] = client; + client.then(c => { + ws.on('message', (m: any) => { + const parsed = JSON.parse(m.toString()); + if (parsed.app === app) { + c.onMessage(JSON.stringify(parsed.payload)); + } + }); + }); + break; + } + case 'disconnect': { + const app = message.app; + (clients[app] || Promise.resolve()).then(c => { + this.removeConnection(c.id); + delete clients[app]; + }); + break; + } + } + }); + + ws.on('close', () => { + cleanup(); + }); + + ws.on('error', () => { + cleanup(); + }); + }); + wss.on('error', (_ws: WebSocket) => { + console.error('error from wss'); + }); + } + _trustedRequestHandler = ( socket: ReactiveSocket, payload: Payload, diff --git a/src/utils/js-client/websocketClientFlipperConnection.tsx b/src/utils/js-client/websocketClientFlipperConnection.tsx new file mode 100644 index 000000000..9e89263d1 --- /dev/null +++ b/src/utils/js-client/websocketClientFlipperConnection.tsx @@ -0,0 +1,83 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import {FlipperClientConnection} from '../../Client'; +import {Payload} from 'rsocket-types'; +import {Flowable, Single} from 'rsocket-flowable'; +import {ConnectionStatus, ISubscriber} from 'rsocket-types'; +import WebSocket from 'ws'; + +export class WebsocketClientFlipperConnection + implements FlipperClientConnection { + websocket: WebSocket; + connStatusSubscribers: Set> = new Set(); + connStatus: ConnectionStatus; + app: string; + plugins: string[] = []; + + constructor(ws: WebSocket, app: string, plugins: string[]) { + this.websocket = ws; + this.connStatus = {kind: 'CONNECTED'}; + this.app = app; + this.plugins = plugins; + } + + connectionStatus(): Flowable { + return new Flowable(subscriber => { + subscriber.onSubscribe({ + cancel: () => { + this.connStatusSubscribers.delete(subscriber); + }, + request: _ => { + this.connStatusSubscribers.add(subscriber); + subscriber.onNext(this.connStatus); + }, + }); + }); + } + + close(): void { + this.connStatus = {kind: 'CLOSED'}; + this.connStatusSubscribers.forEach(subscriber => { + subscriber.onNext(this.connStatus); + }); + this.websocket.send(JSON.stringify({type: 'disconnect', app: this.app})); + } + + fireAndForget(payload: Payload): void { + this.websocket.send( + JSON.stringify({ + type: 'send', + app: this.app, + payload: payload.data != null ? payload.data : {}, + }), + ); + } + + // TODO: fully implement and return actual result + requestResponse(payload: Payload): Single> { + return new Single(subscriber => { + const method = + payload.data != null ? JSON.parse(payload.data).method : 'not-defined'; + subscriber.onSubscribe(() => {}); + if (method != 'getPlugins') { + this.fireAndForget(payload); + } + subscriber.onComplete( + method == 'getPlugins' + ? { + data: JSON.stringify({ + success: {plugins: this.plugins}, + }), + } + : {data: JSON.stringify({success: null})}, + ); + }); + } +} diff --git a/yarn.lock b/yarn.lock index 3b8df88b4..513475c4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1664,6 +1664,13 @@ resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf" integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA== +"@types/ws@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.0.tgz#ed94695be01a77efd590244fc17c3b730e75d88a" + integrity sha512-HnqczxiZ828df9FUh9OyY7vSOelpQNaj+SLEnDvU74rYijp61ggV7dhmDlMky0oYXKLdVuIG4KvExk8DEqzJgQ== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"