Move app/server to flipper-server-core
Summary: moved `app/src/server` to `flipper-server-core/src` and fixed any fallout from that (aka integration points I missed on the preparing diffs). Reviewed By: passy Differential Revision: D31541378 fbshipit-source-id: 8a7e0169ebefa515781f6e5e0f7b926415d4b7e9
This commit is contained in:
committed by
Facebook GitHub Bot
parent
3e7a6b1b4b
commit
d88b28330a
300
desktop/flipper-server-core/src/comms/ServerWebSocket.tsx
Normal file
300
desktop/flipper-server-core/src/comms/ServerWebSocket.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 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 ws from 'ws';
|
||||
import {SecureClientQuery, ServerEventsListener} from './ServerAdapter';
|
||||
import querystring from 'querystring';
|
||||
import {
|
||||
ClientConnection,
|
||||
ConnectionStatus,
|
||||
ConnectionStatusChange,
|
||||
} from './ClientConnection';
|
||||
import {IncomingMessage} from 'http';
|
||||
import {
|
||||
ClientDescription,
|
||||
ClientErrorType,
|
||||
ClientQuery,
|
||||
DeviceOS,
|
||||
} from 'flipper-common';
|
||||
|
||||
/**
|
||||
* WebSocket-based server.
|
||||
*/
|
||||
class ServerWebSocket extends ServerWebSocketBase {
|
||||
constructor(listener: ServerEventsListener) {
|
||||
super(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client verification is not necessary. The connected client has
|
||||
* already been verified using its certificate signed by the server.
|
||||
* @returns
|
||||
*/
|
||||
verifyClient(): ws.VerifyClientCallbackSync {
|
||||
return (_info: {origin: string; req: IncomingMessage; secure: boolean}) => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A connection has been established between the server and a client. Only ever used for
|
||||
* certificate exchange.
|
||||
* @param ws An active WebSocket.
|
||||
* @param message Incoming request message.
|
||||
*/
|
||||
onConnection(ws: WebSocket, message: any): void {
|
||||
const query = querystring.decode(message.url.split('?')[1]);
|
||||
const clientQuery = this._parseClientQuery(query);
|
||||
if (!clientQuery) {
|
||||
console.warn(
|
||||
'[conn] Unable to extract the client query from the request URL.',
|
||||
);
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(
|
||||
`[conn] Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`,
|
||||
clientQuery,
|
||||
);
|
||||
this.listener.onConnectionAttempt(clientQuery);
|
||||
|
||||
ws.on('message', async (message: any) => {
|
||||
const json = JSON.parse(message.toString());
|
||||
const response = await this._onHandleUntrustedMessage(clientQuery, json);
|
||||
if (response) {
|
||||
ws.send(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A secure connection has been established between the server and a client. Once a client
|
||||
* has a valid certificate, it can use a secure connection with Flipper and start exchanging
|
||||
* messages.
|
||||
* @param _ws An active WebSocket.
|
||||
* @param message Incoming request message.
|
||||
*/
|
||||
onSecureConnection(ws: WebSocket, message: any): void {
|
||||
const query = querystring.decode(message.url.split('?')[1]);
|
||||
const clientQuery = this._parseSecureClientQuery(query);
|
||||
if (!clientQuery) {
|
||||
console.warn(
|
||||
'[conn] Unable to extract the client query from the request URL.',
|
||||
);
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
console.info(
|
||||
`[conn] Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`,
|
||||
clientQuery,
|
||||
);
|
||||
this.listener.onSecureConnectionAttempt(clientQuery);
|
||||
|
||||
const pendingRequests: Map<
|
||||
number,
|
||||
{
|
||||
resolve: (data: any) => void;
|
||||
reject: (err: Error) => void;
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
const clientConnection: ClientConnection = {
|
||||
subscribeToEvents(subscriber: ConnectionStatusChange): void {
|
||||
ws.on('close', () => subscriber(ConnectionStatus.CLOSED));
|
||||
ws.on('error', () => subscriber(ConnectionStatus.ERROR));
|
||||
},
|
||||
close(): void {
|
||||
ws.close();
|
||||
},
|
||||
send(data: any): void {
|
||||
ws.send(JSON.stringify(data));
|
||||
},
|
||||
sendExpectResponse(data: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.set(data.id, {reject, resolve});
|
||||
ws.send(JSON.stringify(data));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
let resolvedClient: ClientDescription | undefined;
|
||||
const client: Promise<ClientDescription> =
|
||||
this.listener.onConnectionCreated(clientQuery, clientConnection);
|
||||
client
|
||||
.then((client) => (resolvedClient = client))
|
||||
.catch((e) => {
|
||||
console.error(
|
||||
`[conn] Failed to resolve client ${clientQuery.app} on ${clientQuery.device_id} medium ${clientQuery.medium}`,
|
||||
e,
|
||||
);
|
||||
});
|
||||
|
||||
ws.on('message', (message: any) => {
|
||||
let json: any | undefined;
|
||||
try {
|
||||
json = JSON.parse(message);
|
||||
} catch (err) {
|
||||
console.warn(`Invalid JSON: ${message}`, 'clientMessage');
|
||||
return;
|
||||
}
|
||||
|
||||
const data: {
|
||||
id?: number;
|
||||
success?: Object | undefined;
|
||||
error?: ClientErrorType | undefined;
|
||||
} = json;
|
||||
|
||||
if (data.hasOwnProperty('id') && data.id !== undefined) {
|
||||
const callbacks = pendingRequests.get(data.id);
|
||||
if (!callbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRequests.delete(data.id);
|
||||
|
||||
if (data.success) {
|
||||
callbacks.resolve && callbacks.resolve(data);
|
||||
} else if (data.error) {
|
||||
callbacks.reject && callbacks.reject(data.error);
|
||||
}
|
||||
} else {
|
||||
if (resolvedClient) {
|
||||
this.listener.onClientMessage(resolvedClient.id, message);
|
||||
} else {
|
||||
client &&
|
||||
client
|
||||
.then((client) => {
|
||||
this.listener.onClientMessage(client.id, message);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(
|
||||
'Could not deliver message, client did not resolve. ',
|
||||
e,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a string as being one of those defined as valid OS.
|
||||
* @param str An input string.
|
||||
*/
|
||||
private isOS(str: string): str is DeviceOS {
|
||||
return (
|
||||
str === 'iOS' ||
|
||||
str === 'Android' ||
|
||||
str === 'Metro' ||
|
||||
str === 'Windows' ||
|
||||
str === 'MacOS'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and extract a ClientQuery instance from a message. The ClientQuery
|
||||
* data will be contained in the message url query string.
|
||||
* @param message An incoming web socket message.
|
||||
*/
|
||||
private _parseClientQuery(
|
||||
query: querystring.ParsedUrlQuery,
|
||||
): ClientQuery | undefined {
|
||||
/** Any required arguments to construct a ClientQuery come
|
||||
* embedded in the query string.
|
||||
*/
|
||||
let device_id: string | undefined;
|
||||
if (typeof query.device_id === 'string') {
|
||||
device_id = query.device_id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let device: string | undefined;
|
||||
if (typeof query.device === 'string') {
|
||||
device = query.device;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let app: string | undefined;
|
||||
if (typeof query.app === 'string') {
|
||||
app = query.app;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let os: DeviceOS | undefined;
|
||||
if (typeof query.os === 'string' && this.isOS(query.os)) {
|
||||
os = query.os;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientQuery: ClientQuery = {
|
||||
device_id,
|
||||
device,
|
||||
app,
|
||||
os,
|
||||
};
|
||||
|
||||
if (typeof query.sdk_version === 'string') {
|
||||
const sdk_version = parseInt(query.sdk_version, 10);
|
||||
if (sdk_version) {
|
||||
// TODO: allocate new object, kept now as is to keep changes minimal
|
||||
(clientQuery as any).sdk_version = sdk_version;
|
||||
}
|
||||
}
|
||||
|
||||
return clientQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and extract a SecureClientQuery instance from a message. The ClientQuery
|
||||
* data will be contained in the message url query string.
|
||||
* @param message An incoming web socket message.
|
||||
*/
|
||||
private _parseSecureClientQuery(
|
||||
query: querystring.ParsedUrlQuery,
|
||||
): SecureClientQuery | undefined {
|
||||
/** Any required arguments to construct a SecureClientQuery come
|
||||
* embedded in the query string.
|
||||
*/
|
||||
const clientQuery = this._parseClientQuery(query);
|
||||
if (!clientQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
let csr: string | undefined;
|
||||
if (typeof query.csr === 'string') {
|
||||
const buffer = Buffer.from(query.csr, 'base64');
|
||||
if (buffer) {
|
||||
csr = buffer.toString('ascii');
|
||||
}
|
||||
}
|
||||
|
||||
let csr_path: string | undefined;
|
||||
if (typeof query.csr_path === 'string') {
|
||||
csr_path = query.csr_path;
|
||||
}
|
||||
|
||||
let medium: number | undefined;
|
||||
if (typeof query.medium === 'string') {
|
||||
medium = parseInt(query.medium, 10);
|
||||
}
|
||||
if (medium !== undefined && (medium < 1 || medium > 3)) {
|
||||
throw new Error('Unsupported exchange medium: ' + medium);
|
||||
}
|
||||
return {...clientQuery, csr, csr_path, medium: medium as any};
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerWebSocket;
|
||||
Reference in New Issue
Block a user