Fix Android Connection Swap Issue (Server Side)

Summary:
- Take device id retrieved by matching CSR to the given CSR destination
  - Otherwise, use the previous way

Note:
- Backward compatibility will be fixed next diff

Reviewed By: jknoxville

Differential Revision: D17346422

fbshipit-source-id: 59b2fb9849373db1ba930dde702194c5fb201678
This commit is contained in:
Chaiwat Ekkaewnumchai
2019-09-16 05:31:10 -07:00
committed by Facebook Github Bot
parent 88197a7076
commit 2239ca65a5
3 changed files with 84 additions and 49 deletions

View File

@@ -112,7 +112,6 @@ export default class Client extends EventEmitter {
lessPlugins: Plugins | undefined; lessPlugins: Plugins | undefined;
showAllPlugins: boolean; showAllPlugins: boolean;
connection: RSocketClientSocket<any, any> | null | undefined; connection: RSocketClientSocket<any, any> | null | undefined;
responder: Partial<Responder<string, any>>;
store: Store; store: Store;
activePlugins: Set<string>; activePlugins: Set<string>;
device: Promise<BaseDevice>; device: Promise<BaseDevice>;
@@ -121,6 +120,7 @@ export default class Client extends EventEmitter {
logger: Logger; logger: Logger;
lastSeenDeviceList: Array<BaseDevice>; lastSeenDeviceList: Array<BaseDevice>;
broadcastCallbacks: Map<string, Map<string, Set<Function>>>; broadcastCallbacks: Map<string, Map<string, Set<Function>>>;
rIC: any;
requestCallbacks: Map< requestCallbacks: Map<
number, number,
@@ -162,19 +162,12 @@ export default class Client extends EventEmitter {
const client = this; const client = this;
// node.js doesn't support requestIdleCallback // node.js doesn't support requestIdleCallback
const rIC = this.rIC =
typeof window === 'undefined' typeof window === 'undefined'
? (cb: Function, _: any) => { ? (cb: Function, _: any) => {
cb(); cb();
} }
: window.requestIdleCallback; : window.requestIdleCallback.bind(window);
this.responder = {
fireAndForget: (payload: {data: string}) =>
rIC(() => client.onMessage(payload.data), {
timeout: 500,
}),
};
if (conn) { if (conn) {
conn.connectionStatus().subscribe({ conn.connectionStatus().subscribe({

View File

@@ -28,6 +28,11 @@ type ClientInfo = {
client: Client; client: Client;
}; };
type ClientCsrQuery = {
csr?: string | undefined;
csr_path?: string | undefined;
};
declare interface Server { declare interface Server {
on(event: 'new-client', callback: (client: Client) => void): this; on(event: 'new-client', callback: (client: Client) => void): this;
on(event: 'error', callback: (err: Error) => void): this; on(event: 'error', callback: (err: Error) => void): this;
@@ -124,16 +129,24 @@ class Server extends EventEmitter {
if (!payload.data) { if (!payload.data) {
return {}; return {};
} }
const clientData: ClientQuery = JSON.parse(payload.data); const clientData: ClientQuery & ClientCsrQuery = JSON.parse(payload.data);
this.connectionTracker.logConnectionAttempt(clientData); this.connectionTracker.logConnectionAttempt(clientData);
const client = this.addConnection(socket, clientData); const {app, os, device, device_id, sdk_version, csr, csr_path} = clientData;
const client: Promise<Client> = this.addConnection(
socket,
{app, os, device, device_id, sdk_version},
{csr, csr_path},
);
socket.connectionStatus().subscribe({ socket.connectionStatus().subscribe({
onNext(payload) { onNext(payload) {
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') { if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
console.debug(`Device disconnected ${client.id}`, 'server'); client.then(client => {
server.removeConnection(client.id); console.debug(`Device disconnected ${client.id}`, 'server');
server.removeConnection(client.id);
});
} }
}, },
onSubscribe(subscription) { onSubscribe(subscription) {
@@ -141,7 +154,14 @@ class Server extends EventEmitter {
}, },
}); });
return client.responder; return {
fireAndForget: (payload: {data: string}) =>
client.then(client => {
client.rIC(() => client.onMessage(payload.data), {
timeout: 500,
});
}),
};
}; };
_untrustedRequestHandler = ( _untrustedRequestHandler = (
@@ -272,49 +292,66 @@ class Server extends EventEmitter {
return null; return null;
} }
addConnection( async addConnection(
conn: RSocketClientSocket<any, any>, conn: RSocketClientSocket<any, any>,
query: ClientQuery, query: ClientQuery,
): Client { csrQuery: ClientCsrQuery,
): Promise<Client> {
invariant(query, 'expected query'); invariant(query, 'expected query');
const id = `${query.app}#${query.os}#${query.device}#${query.device_id}`; // try to get id by comparing giving `csr` to file from `csr_path`
console.debug(`Device connected: ${id}`, 'server'); // otherwise, use given device_id
const {csr_path, csr} = csrQuery;
return (csr_path && csr
? this.certificateProvider.extractAppNameFromCSR(csr).then(appName => {
return this.certificateProvider.getTargetDeviceId(
query.os,
appName,
csr_path,
csr,
);
})
: Promise.resolve(query.device_id)
).then(csrId => {
query.device_id = csrId;
const id = `${query.app}#${query.os}#${query.device}#${csrId}`;
console.debug(`Device connected: ${id}`, 'server');
const client = new Client(id, query, conn, this.logger, this.store); const client = new Client(id, query, conn, this.logger, this.store);
const info = { const info = {
client, client,
connection: conn, connection: conn,
}; };
client.init().then(() => { client.init().then(() => {
console.debug( console.debug(
`Device client initialised: ${id}. Supported plugins: ${client.plugins.join( `Device client initialised: ${id}. Supported plugins: ${client.plugins.join(
', ', ', ',
)}`, )}`,
'server', 'server',
); );
/* If a device gets disconnected without being cleaned up properly, /* If a device gets disconnected without being cleaned up properly,
* Flipper won't be aware until it attempts to reconnect. * Flipper won't be aware until it attempts to reconnect.
* When it does we need to terminate the zombie connection. * When it does we need to terminate the zombie connection.
*/ */
if (this.connections.has(id)) { if (this.connections.has(id)) {
const connectionInfo = this.connections.get(id); const connectionInfo = this.connections.get(id);
connectionInfo && connectionInfo &&
connectionInfo.connection && connectionInfo.connection &&
connectionInfo.connection.close(); connectionInfo.connection.close();
this.removeConnection(id); this.removeConnection(id);
} }
this.connections.set(id, info); this.connections.set(id, info);
this.emit('new-client', client); this.emit('new-client', client);
this.emit('clients-change'); this.emit('clients-change');
client.emit('plugins-change'); client.emit('plugins-change');
});
return client;
}); });
return client;
} }
attachFakeClient(client: Client) { attachFakeClient(client: Client) {

View File

@@ -346,7 +346,12 @@ export default class CertificateProvider {
return androidUtil return androidUtil
.pull(deviceId, processName, directory + csrFileName) .pull(deviceId, processName, directory + csrFileName)
.then(deviceCsr => { .then(deviceCsr => {
return this.santitizeString(deviceCsr.toString()) === csr; // Santitize both of the string before comparation
// The csr string extraction on client side return string in both way
return (
this.santitizeString(deviceCsr.toString()) ===
this.santitizeString(csr)
);
}); });
} }