WebSocket-based server, base class and browser
Summary: Introduce a base class for WebSocket based servers and a Browser-based implementation which is in use by Kite. The implementation for the Browser-based one is basically taken as is from ServerController but slightly adapted to match the existing interface. As with the RSocket-based implementation, this diff doesn't put this implementation into use but is a good opportunity to revisit the existing implementation. Reviewed By: passy Differential Revision: D29985886 fbshipit-source-id: 32abba37ec31478b6497ef5cfe90bb9aedc282d3
This commit is contained in:
committed by
Facebook GitHub Bot
parent
e2fc1339f4
commit
5c99170abd
113
desktop/app/src/comms/ServerWebSocketBase.tsx
Normal file
113
desktop/app/src/comms/ServerWebSocketBase.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 {IncomingMessage} from 'http';
|
||||
import {SecureServerConfig} from '../utils/CertificateProvider';
|
||||
import ServerAdapter, {ServerEventsListener} from './ServerAdapter';
|
||||
import ws from 'ws';
|
||||
import WebSocket from 'ws';
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
|
||||
/**
|
||||
* It serves as a base class for WebSocket based servers. It delegates the 'connection'
|
||||
* event to subclasses as a customisation point.
|
||||
*/
|
||||
abstract class ServerWebSocketBase extends ServerAdapter {
|
||||
rawServer_: ws.Server | null;
|
||||
constructor(listener: ServerEventsListener) {
|
||||
super(listener);
|
||||
this.rawServer_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket client verification. Usually used to validate the origin.
|
||||
*
|
||||
* Base implementation simply returns true, but this can be overriden by subclasses
|
||||
* that require verification.
|
||||
*
|
||||
* @returns Return true if the client was successfully verified, otherwise
|
||||
* returns false.
|
||||
*/
|
||||
verifyClient(): ws.VerifyClientCallbackSync {
|
||||
return (_info: {origin: string; req: IncomingMessage; secure: boolean}) => {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
start(port: number, sslConfig?: SecureServerConfig): Promise<boolean> {
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
let server: http.Server | https.Server | undefined;
|
||||
if (sslConfig) {
|
||||
server = https.createServer({
|
||||
key: sslConfig.key,
|
||||
cert: sslConfig.cert,
|
||||
ca: sslConfig.ca,
|
||||
// Client to provide a certificate to authenticate.
|
||||
requestCert: sslConfig.requestCert,
|
||||
// As specified as "true", so no unauthenticated traffic
|
||||
// will make it to the specified route specified
|
||||
rejectUnauthorized: sslConfig.rejectUnauthorized,
|
||||
});
|
||||
} else {
|
||||
server = http.createServer();
|
||||
}
|
||||
|
||||
const handleRequest = sslConfig
|
||||
? self.onSecureConnection
|
||||
: self.onConnection;
|
||||
|
||||
const rawServer = new WebSocket.Server({
|
||||
server,
|
||||
verifyClient: this.verifyClient(),
|
||||
});
|
||||
rawServer.on('connection', (ws: WebSocket, message: any) => {
|
||||
handleRequest.apply(self, [ws, message]);
|
||||
});
|
||||
rawServer.on('error', (_ws: WebSocket, error: any) => {
|
||||
console.warn('Server found connection error: ' + error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (server) {
|
||||
server.listen(port, () => {
|
||||
console.debug(
|
||||
`${
|
||||
sslConfig ? 'Secure' : 'Certificate'
|
||||
} server started on port ${port}`,
|
||||
'server',
|
||||
);
|
||||
self.listener.onListening(port);
|
||||
self.rawServer_ = rawServer;
|
||||
resolve(true);
|
||||
});
|
||||
} else {
|
||||
reject(new Error(`Unable to start server at port ${port}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
if (this.rawServer_) {
|
||||
return Promise.resolve(this.rawServer_.close());
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
onConnection(_ws: WebSocket, _message: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
onSecureConnection(_ws: WebSocket, _message: any): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerWebSocketBase;
|
||||
162
desktop/app/src/comms/ServerWebSocketBrowser.tsx
Normal file
162
desktop/app/src/comms/ServerWebSocketBrowser.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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 ServerWebSocketBase from './ServerWebSocketBase';
|
||||
import WebSocket from 'ws';
|
||||
import querystring from 'querystring';
|
||||
import Client, {ClientQuery} from '../Client';
|
||||
import {BrowserClientFlipperConnection} from './BrowserClientFlipperConnection';
|
||||
import {ServerEventsListener} from './ServerAdapter';
|
||||
import constants from '../fb-stubs/constants';
|
||||
import ws from 'ws';
|
||||
import {IncomingMessage} from 'http';
|
||||
|
||||
/**
|
||||
* WebSocket-based server which uses a connect/disconnect handshake over an insecure channel.
|
||||
*/
|
||||
class ServerWebSocketBrowser extends ServerWebSocketBase {
|
||||
constructor(listener: ServerEventsListener) {
|
||||
super(listener);
|
||||
}
|
||||
|
||||
verifyClient(): ws.VerifyClientCallbackSync {
|
||||
return (info: {origin: string; req: IncomingMessage; secure: boolean}) => {
|
||||
return constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES.some(
|
||||
(validPrefix) => info.origin.startsWith(validPrefix),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A connection has been established between the server and a client.
|
||||
* @param ws An active WebSocket.
|
||||
* @param message Incoming request message.
|
||||
*/
|
||||
onConnection(ws: WebSocket, message: any): void {
|
||||
const clients: {
|
||||
[app: string]: Promise<Client>;
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* Any required arguments to construct a ClientQuery come
|
||||
* embedded in the query string.
|
||||
*/
|
||||
const query = querystring.decode(message.url.split('?')[1]);
|
||||
const deviceId: string =
|
||||
typeof query.deviceId === 'string' ? query.deviceId : 'webbrowser';
|
||||
const device =
|
||||
typeof query.device === 'string' ? query.device : 'WebSocket';
|
||||
|
||||
const clientQuery: ClientQuery = {
|
||||
device_id: deviceId,
|
||||
device,
|
||||
app: device,
|
||||
os: 'JSWebApp',
|
||||
};
|
||||
|
||||
this.listener.onConnectionAttempt(clientQuery);
|
||||
|
||||
const cleanup = () => {
|
||||
Object.values(clients).map((p) =>
|
||||
p.then((c) => this.listener.onConnectionClosed(c.id)),
|
||||
);
|
||||
// TODO: destroy device.
|
||||
// This seems to be the only case in which a device gets destroyed when there's a disconnection
|
||||
// or error on the transport layer.
|
||||
//
|
||||
// destroyDevice(this.store, this.logger, deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to the 'message' event. Initially, a handshake should take place in the form of a
|
||||
* 'connect' message. Once received, a client connection will be established and registered. This
|
||||
* is followed by another subscription to the 'message' event, again. Effectively, two listeners
|
||||
* are now attached to that event. The former will continue to check for 'connect' and 'disconnect'
|
||||
* messages. The latter will deliver messages to the client.
|
||||
*/
|
||||
ws.on('message', (rawMessage: any) => {
|
||||
let message: any | undefined;
|
||||
try {
|
||||
message = JSON.parse(rawMessage.toString());
|
||||
} catch (error) {
|
||||
// Throws a SyntaxError exception if the string to parse is not valid JSON.
|
||||
console.log('Received message is not valid.', error);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case 'connect': {
|
||||
const app = message.app;
|
||||
const plugins = message.plugins;
|
||||
|
||||
const clientConnection = new BrowserClientFlipperConnection(
|
||||
ws,
|
||||
app,
|
||||
plugins,
|
||||
);
|
||||
|
||||
const extendedClientQuery = {...clientQuery, medium: 1};
|
||||
extendedClientQuery.sdk_version = plugins == null ? 4 : 1;
|
||||
|
||||
let resolvedClient: Client | null = null;
|
||||
const client: Promise<Client> = this.listener.onConnectionCreated(
|
||||
extendedClientQuery,
|
||||
clientConnection,
|
||||
);
|
||||
client.then((client) => (resolvedClient = client)).catch((_) => {});
|
||||
|
||||
clients[app] = client;
|
||||
|
||||
ws.on('message', (m: any) => {
|
||||
let parsed: any | undefined;
|
||||
try {
|
||||
parsed = JSON.parse(m.toString());
|
||||
} catch (error) {
|
||||
// Throws a SyntaxError exception if the string to parse is not valid JSON.
|
||||
console.log('Received message is not valid.', error);
|
||||
return;
|
||||
}
|
||||
// non-null payload id means response to prev request, it's handled in connection
|
||||
if (parsed.app === app && parsed.payload?.id == null) {
|
||||
const message = JSON.stringify(parsed.payload);
|
||||
if (resolvedClient) {
|
||||
resolvedClient.onMessage(message);
|
||||
} else {
|
||||
client.then((c) => c.onMessage(message)).catch((_) => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'disconnect': {
|
||||
const app = message.app;
|
||||
(clients[app] || Promise.resolve())
|
||||
.then((c) => {
|
||||
this.listener.onConnectionClosed(c.id);
|
||||
delete clients[app];
|
||||
})
|
||||
.catch((_) => {});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/** Close event from the existing client connection. */
|
||||
ws.on('close', () => {
|
||||
cleanup();
|
||||
});
|
||||
/** Error event from the existing client connection. */
|
||||
ws.on('error', (error) => {
|
||||
console.warn('Server found connection error: ' + error);
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerWebSocketBrowser;
|
||||
Reference in New Issue
Block a user