Migrate from socket.io

Reviewed By: passy

Differential Revision: D34787674

fbshipit-source-id: 63d7c166ea29d14c96f0646a045e3f6fa93472e2
This commit is contained in:
Andrey Goncharov
2022-03-10 10:31:24 -08:00
committed by Facebook GitHub Bot
parent 6ec3771824
commit f85def32fb
11 changed files with 307 additions and 210 deletions

View File

@@ -11,7 +11,8 @@
"dependenciesComment": "mac-ca is required dynamically for darwin, node-fetch is treated special in electron-requires, not sure why",
"dependencies": {
"mac-ca": "^1.0.6",
"node-fetch": "^2.6.7"
"node-fetch": "^2.6.7",
"ws": "^8.5.0"
},
"devDependencies": {
"@types/express": "^4.17.13",
@@ -25,7 +26,6 @@
"metro": "^0.69.0",
"open": "^8.3.0",
"p-filter": "^2.1.0",
"socket.io": "^4.4.1",
"yargs": "^17.0.1"
},
"peerDependencies": {},

View File

@@ -11,8 +11,9 @@ import express, {Express} from 'express';
import http from 'http';
import path from 'path';
import fs from 'fs-extra';
import socketio from 'socket.io';
import {VerifyClientCallbackSync, WebSocketServer} from 'ws';
import {WEBSOCKET_MAX_MESSAGE_SIZE} from 'flipper-server-core';
import {parse} from 'url';
type Config = {
port: number;
@@ -23,7 +24,7 @@ type Config = {
export async function startBaseServer(config: Config): Promise<{
app: Express;
server: http.Server;
socket: socketio.Server;
socket: WebSocketServer;
}> {
const {app, server} = await startAssetServer(config);
const socket = addWebsocket(server, config);
@@ -67,36 +68,60 @@ function addWebsocket(server: http.Server, config: Config) {
const localhostIPV6NoBrackets = `::1:${config.port}`;
const possibleHosts = [localhostIPV4, localhostIPV6, localhostIPV6NoBrackets];
const possibleOrigins = possibleHosts.map((host) => `http://${host}`);
const io = new socketio.Server(server, {
maxHttpBufferSize: WEBSOCKET_MAX_MESSAGE_SIZE,
allowRequest(req, callback) {
const noOriginHeader = req.headers.origin === undefined;
if (
noOriginHeader &&
req.headers.host &&
possibleHosts.includes(req.headers.host)
) {
// no origin header? Either the request is not cross-origin,
// or the request is not originating from a browser, so should be OK to pass through
callback(null, true);
} else {
// for now we don't allow cross origin request, so that an arbitrary website cannot try to
// connect a socket to localhost:serverport, and try to use the all powerful Flipper APIs to read
// for example files.
// Potentially in the future we do want to allow this, e.g. if we want to connect to a local flipper-server
// directly from intern. But before that, we should either authenticate the request somehow,
// and discuss security impact and for example scope the files that can be read by Flipper.
console.warn(
`Refused sockect connection from cross domain request, origin: ${
req.headers.origin
}, host: ${req.headers.host}. Expected: ${possibleHosts.join(
' or ',
)}`,
);
callback(null, false);
}
},
const verifyClient: VerifyClientCallbackSync = ({origin, req}) => {
const noOriginHeader = origin === undefined;
if (
(noOriginHeader || possibleOrigins.includes(origin)) &&
req.headers.host &&
possibleHosts.includes(req.headers.host)
) {
// no origin header? The request is not originating from a browser, so should be OK to pass through
// If origin matches our own address, it means we are serving the page
return true;
} else {
// for now we don't allow cross origin request, so that an arbitrary website cannot try to
// connect a socket to localhost:serverport, and try to use the all powerful Flipper APIs to read
// for example files.
// Potentially in the future we do want to allow this, e.g. if we want to connect to a local flipper-server
// directly from intern. But before that, we should either authenticate the request somehow,
// and discuss security impact and for example scope the files that can be read by Flipper.
console.warn(
`Refused socket connection from cross domain request, origin: ${origin}, host: ${
req.headers.host
}. Expected origins: ${possibleOrigins.join(
' or ',
)}. Expected hosts: ${possibleHosts.join(' or ')}`,
);
return false;
}
};
const wss = new WebSocketServer({
noServer: true,
maxPayload: WEBSOCKET_MAX_MESSAGE_SIZE,
verifyClient,
});
return io;
server.on('upgrade', function upgrade(request, socket, head) {
const {pathname} = parse(request.url);
// Handled by Metro
if (pathname === '/hot') {
return;
}
if (pathname === '/') {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request);
});
return;
}
console.error('addWebsocket.upgrade -> unknown pathname', pathname);
socket.destroy();
});
return wss;
}

View File

@@ -8,53 +8,91 @@
*/
import chalk from 'chalk';
import {
ClientWebSocketMessage,
ExecResponseWebSocketMessage,
ExecResponseErrorWebSocketMessage,
ServerEventWebSocketMessage,
} from 'flipper-common';
import {FlipperServerImpl} from 'flipper-server-core';
import socketio from 'socket.io';
import {WebSocketServer} from 'ws';
export function startSocketServer(
flipperServer: FlipperServerImpl,
socket: socketio.Server,
socket: WebSocketServer,
) {
socket.on('connection', (client) => {
console.log(chalk.green(`Client connected ${client.id}`));
socket.on('connection', (client, req) => {
const clientAddress = `${req.socket.remoteAddress}:${req.socket.remotePort}`;
console.log(chalk.green(`Client connected ${clientAddress}`));
let connected = true;
function onServerEvent(event: string, payoad: any) {
client.emit('event', event, payoad);
function onServerEvent(event: string, payload: any) {
const message = {
event: 'server-event',
payload: {
event,
data: payload,
},
} as ServerEventWebSocketMessage;
client.send(JSON.stringify(message));
}
flipperServer.onAny(onServerEvent);
client.on('exec', (id, command, args) => {
flipperServer
.exec(command, ...args)
.then((result: any) => {
if (connected) {
client.emit('exec-response', id, result);
}
})
.catch((error: any) => {
if (connected) {
// TODO: Serialize error
// TODO: log if verbose console.warn('Failed to handle response', error);
client.emit(
'exec-response-error',
id,
error.toString() + (error.stack ? `\n${error.stack}` : ''),
);
}
});
client.on('message', (data) => {
const {event, payload} = JSON.parse(
data.toString(),
) as ClientWebSocketMessage;
switch (event) {
case 'exec': {
const {id, command, args} = payload;
flipperServer
.exec(command, ...args)
.then((result: any) => {
if (connected) {
const response: ExecResponseWebSocketMessage = {
event: 'exec-response',
payload: {
id,
data: result,
},
};
client.send(JSON.stringify(response));
}
})
.catch((error: any) => {
if (connected) {
// TODO: Serialize error
// TODO: log if verbose console.warn('Failed to handle response', error);
const responseError: ExecResponseErrorWebSocketMessage = {
event: 'exec-response-error',
payload: {
id,
data:
error.toString() +
(error.stack ? `\n${error.stack}` : ''),
},
};
client.send(JSON.stringify(responseError));
}
});
}
}
});
client.on('disconnect', () => {
console.log(chalk.red(`Client disconnected ${client.id}`));
client.on('close', () => {
console.log(chalk.red(`Client disconnected ${clientAddress}`));
connected = false;
flipperServer.offAny(onServerEvent);
});
client.on('error', (e) => {
console.error(chalk.red(`Socket error ${client.id}`), e);
console.error(chalk.red(`Socket error ${clientAddress}`), e);
connected = false;
flipperServer.offAny(onServerEvent);
});

View File

@@ -12,7 +12,7 @@ import {Express} from 'express';
import http from 'http';
import path from 'path';
import fs from 'fs-extra';
import socketio from 'socket.io';
import {WebSocketServer} from 'ws';
import pFilter from 'p-filter';
import {homedir} from 'os';
@@ -51,7 +51,7 @@ export async function getPluginSourceFolders(): Promise<string[]> {
export async function startWebServerDev(
app: Express,
server: http.Server,
socket: socketio.Server,
socket: WebSocketServer,
rootDir: string,
) {
// prevent bundling!
@@ -138,7 +138,11 @@ export async function startWebServerDev(
connectMiddleware.attachHmrServer(server);
app.use(function (err: any, _req: any, _res: any, next: any) {
console.error(chalk.red('\n\nCompile error in client bundle\n'), err);
socket.local.emit('hasErrors', err.toString());
socket.clients.forEach((client) => {
client.send(
JSON.stringify({event: 'hasErrors', payload: err.toString()}),
);
});
next();
});