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:
Michel Weststrate
2021-10-12 15:59:44 -07:00
committed by Facebook GitHub Bot
parent 3e7a6b1b4b
commit d88b28330a
73 changed files with 563 additions and 534 deletions

View 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;