Files
flipper/desktop/flipper-server-core/src/comms/SecureServerWebSocket.tsx
Andrey Goncharov 37498ad5a9 Refactor server implementation for WebSockets
Summary:
Standardize WS implementation for JS environments.

Why do we need a separate server implementation for browsers?
Browser targets cannot authenticate via the default certificate exchange flow. For browser targets we verify the origin instead.
Moreover, for already forgotten reasons the initial implementation of the WS server for browsers used a different kind of message structure and added extra `connect`/`disconnect` messages. After examination, it seems the `connect`/`disconnect` flow is redundant.

Major changes:
1. Updated class hierarchy for WS server implementations.
2. Updated browser WS server to support the modern and the legacy protocols.
3. Now a websocket connection with the device is closed on error. The idea is it is highly unlikely to handle any subsequent messages properly once we observe an error. It is better to bail and reconnect. What do you think?

Reviewed By: mweststrate

Differential Revision: D31532172

fbshipit-source-id: f86aa63a40efe4d5263353cc124fac8c63b80e45
2021-10-21 03:34:15 -07:00

136 lines
4.3 KiB
TypeScript

/**
* 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 ServerWebSocket, {ConnectionCtx} from './ServerWebSocket';
import {SecureClientQuery} from './ServerAdapter';
import {ParsedUrlQuery} from 'querystring';
import {ClientDescription} from 'flipper-common';
import {
isWsResponseMessage,
parseSecureClientQuery,
assertNotNull,
} from './Utilities';
import WebSocketClientConnection from './WebSocketClientConnection';
import {serializeError} from 'serialize-error';
import {WSCloseCode} from '../utils/WSCloseCode';
export interface SecureConnectionCtx extends ConnectionCtx {
clientQuery?: SecureClientQuery;
clientConnection?: WebSocketClientConnection;
clientPromise?: Promise<ClientDescription>;
client?: ClientDescription;
}
/**
* WebSocket-based server.
* 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.
* https://fbflipper.com/docs/extending/new-clients
* https://fbflipper.com/docs/extending/establishing-a-connection
*/
class SecureServerWebSocket extends ServerWebSocket {
protected handleConnectionAttempt(ctx: SecureConnectionCtx): void {
const {clientQuery, ws} = ctx;
assertNotNull(clientQuery);
console.info(
`[conn] Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`,
);
this.listener.onSecureConnectionAttempt(clientQuery);
const clientConnection = new WebSocketClientConnection(ws);
// TODO: Could we just await it here? How much time could it be, potentially?
// DRI: @aigoncharov
const clientPromise: Promise<ClientDescription> = this.listener
.onConnectionCreated(clientQuery, clientConnection)
.then((client) => {
ctx.client = client;
return client;
})
.catch((e) => {
throw new Error(
`Failed to resolve client ${clientQuery.app} on ${
clientQuery.device_id
} medium ${clientQuery.medium}. Reason: ${JSON.stringify(
serializeError(e),
)}`,
);
});
ctx.clientConnection = clientConnection;
ctx.clientPromise = clientPromise;
}
protected async handleMessage(
ctx: SecureConnectionCtx,
parsedMessage: object,
rawMessage: string,
) {
const {clientQuery, clientConnection, clientPromise, client, ws} = ctx;
assertNotNull(clientQuery);
assertNotNull(clientConnection);
assertNotNull(clientPromise);
// We can recieve either "execute" messages from the client or "responses" to our messages
// https://fbflipper.com/docs/extending/new-clients#responding-to-messages
// Received a response message
if (isWsResponseMessage(parsedMessage)) {
const callbacks = clientConnection.matchPendingRequest(parsedMessage.id);
if (parsedMessage.success !== undefined) {
callbacks.resolve({
...parsedMessage,
length: rawMessage.length,
});
return;
}
callbacks.reject(parsedMessage.error);
return;
}
// Received an "execute" message
if (client) {
this.listener.onClientMessage(client.id, rawMessage);
} else {
// Client promise is not resolved yet
// So we schedule the execution for when it is resolved
clientPromise
.then((client) => {
this.listener.onClientMessage(client.id, rawMessage);
})
.catch((error) => {
// It is an async action, which might run after the socket is closed
if (ws.readyState === ws.OPEN) {
// See the reasoning in the error handler for a `connection` event in ServerWebSocket
ws.emit('error', error);
ws.close(WSCloseCode.InternalError);
}
});
}
}
/**
* 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.
*/
protected parseClientQuery(
query: ParsedUrlQuery,
): SecureClientQuery | undefined {
return parseSecureClientQuery(query);
}
}
export default SecureServerWebSocket;