Code improvements and more logging on connection handling

Summary:
In an attempt to trace Android issues:

1. added more logging to the process (opted for info level for now since this is pretty critical for support requests, yet not super repetitive overall. We could maybe turn it into usage tracking at some point to have central stats?).
2. rewrote promise chains to async/await since they are easier to follow and harder to do accidentally wrong
3. fixed some minor potential problems, will highlights those in code.

Changelog: Improved handling of edge cases in certificate exchange, which should address cases where a Flipper connection wouldn't come up when connection to Android / IOS. Added explicit logging around connection negation.

Reviewed By: lblasa

Differential Revision: D30838947

fbshipit-source-id: a898c6d3be6edc22bd24f9d2bad76e81871360da
This commit is contained in:
Michel Weststrate
2021-09-10 07:01:41 -07:00
committed by Facebook GitHub Bot
parent 0816f73d07
commit d8f77db632
8 changed files with 318 additions and 265 deletions

View File

@@ -138,10 +138,11 @@ abstract class ServerAdapter {
async _onHandleUntrustedMessage( async _onHandleUntrustedMessage(
clientQuery: ClientQuery, clientQuery: ClientQuery,
rawData: any, rawData: any,
): Promise<any> { ): Promise<string | undefined> {
// OSS's older Client SDK might not send medium information. // OSS's older Client SDK might not send medium information.
// This is not an issue for internal FB users, as Flipper release // This is not an issue for internal FB users, as Flipper release
// is insync with client SDK through launcher. // is insync with client SDK through launcher.
const message: { const message: {
method: 'signCertificate'; method: 'signCertificate';
csr: string; csr: string;
@@ -149,24 +150,37 @@ abstract class ServerAdapter {
medium: number | undefined; medium: number | undefined;
} = rawData; } = rawData;
console.log(
`[conn] Connection attempt: ${clientQuery.app} on ${clientQuery.device}, medium: ${message.medium}, cert: ${message.destination}`,
clientQuery,
rawData,
);
if (message.method === 'signCertificate') { if (message.method === 'signCertificate') {
console.debug('CSR received from device', 'server'); console.debug('CSR received from device', 'server');
const {csr, destination, medium} = message; const {csr, destination, medium} = message;
console.log(
`[conn] Starting certificate exchange: ${clientQuery.app} on ${clientQuery.device}`,
);
const result = await this.listener.onProcessCSR( const result = await this.listener.onProcessCSR(
csr, csr,
clientQuery, clientQuery,
destination, destination,
transformCertificateExchangeMediumToType(medium), transformCertificateExchangeMediumToType(medium),
); );
console.log(
`[conn] Exchanged certificate: ${clientQuery.app} on ${clientQuery.device_id}`,
);
const response = JSON.stringify({ const response = JSON.stringify({
deviceId: result.deviceId, deviceId: result.deviceId,
}); });
return response; return response;
} }
return Promise.resolve(); return undefined;
} }
} }

View File

@@ -151,6 +151,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
const {app, os, device, device_id, sdk_version, csr, csr_path, medium} = const {app, os, device, device_id, sdk_version, csr, csr_path, medium} =
clientQuery; clientQuery;
const transformedMedium = transformCertificateExchangeMediumToType(medium); const transformedMedium = transformCertificateExchangeMediumToType(medium);
console.log(
`[conn] Connection established: ${app} on ${device_id}. Medium ${medium}. CSR: ${csr_path}`,
);
return this.addConnection( return this.addConnection(
clientConnection, clientConnection,
{ {
@@ -293,6 +296,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
csr_path, csr_path,
csr, csr,
); );
console.log(
`[conn] Detected ${app_name} on ${query.device_id} in certificate`,
);
} }
query.app = appNameWithUpdateHint(query); query.app = appNameWithUpdateHint(query);
@@ -303,8 +309,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
device: query.device, device: query.device,
device_id: query.device_id, device_id: query.device_id,
}); });
console.debug(`Device connected: ${id}`, 'server'); console.log(
`[conn] Matching device for ${query.app} on ${query.device_id}...`,
);
const device = const device =
getDeviceBySerial(this.store.getState(), query.device_id) ?? getDeviceBySerial(this.store.getState(), query.device_id) ??
(await findDeviceForConnection(this.store, query.app, query.device_id)); (await findDeviceForConnection(this.store, query.app, query.device_id));
@@ -324,6 +331,10 @@ class ServerController extends EventEmitter implements ServerEventsListener {
connection: connection, connection: connection,
}; };
console.log(
`[conn] Initializing client ${query.app} on ${query.device_id}...`,
);
await client.init(); await client.init();
connection.subscribeToEvents((status: ConnectionStatus) => { connection.subscribeToEvents((status: ConnectionStatus) => {
@@ -336,7 +347,7 @@ class ServerController extends EventEmitter implements ServerEventsListener {
}); });
console.debug( console.debug(
`Device client initialized: ${id}. Supported plugins: ${Array.from( `[conn] Device client initialized: ${id}. Supported plugins: ${Array.from(
client.plugins, client.plugins,
).join(', ')}`, ).join(', ')}`,
'server', 'server',
@@ -382,6 +393,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
removeConnection(id: string) { removeConnection(id: string) {
const info = this.connections.get(id); const info = this.connections.get(id);
if (info) { if (info) {
console.log(
`[conn] Disconnected: ${info.client.query.app} on ${info.client.query.device_id}.`,
);
info.client.disconnect(); info.client.disconnect();
this.connections.delete(id); this.connections.delete(id);
this.emit('clients-change'); this.emit('clients-change');
@@ -412,7 +426,7 @@ class ConnectionTracker {
this.connectionAttempts.set(key, entry); this.connectionAttempts.set(key, entry);
if (entry.length >= this.connectionProblemThreshold) { if (entry.length >= this.connectionProblemThreshold) {
console.error( console.error(
`Connection loop detected with ${key}. Connected ${ `[conn] Connection loop detected with ${key}. Connected ${
this.connectionProblemThreshold this.connectionProblemThreshold
} times within ${this.timeWindowMillis / 1000}s.`, } times within ${this.timeWindowMillis / 1000}s.`,
'server', 'server',
@@ -444,7 +458,10 @@ async function findDeviceForConnection(
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
unsubscribe(); unsubscribe();
const error = `Timed out waiting for device ${serial} for client ${clientId}`; const error = `Timed out waiting for device ${serial} for client ${clientId}`;
console.error('Unable to find device for connection. Error:', error); console.error(
'[conn] Unable to find device for connection. Error:',
error,
);
reject(error); reject(error);
}, 15000); }, 15000);
unsubscribe = sideEffect( unsubscribe = sideEffect(
@@ -460,6 +477,7 @@ async function findDeviceForConnection(
(device) => device.serial === serial, (device) => device.serial === serial,
); );
if (matchingDevice) { if (matchingDevice) {
console.log(`[conn] Found device for: ${clientId} on ${serial}.`);
clearTimeout(timeout); clearTimeout(timeout);
resolve(matchingDevice); resolve(matchingDevice);
unsubscribe(); unsubscribe();

View File

@@ -109,6 +109,9 @@ class ServerRSocket extends ServerAdapter {
const clientQuery: SecureClientQuery = JSON.parse(payload.data); const clientQuery: SecureClientQuery = JSON.parse(payload.data);
this.listener.onSecureConnectionAttempt(clientQuery); this.listener.onSecureConnectionAttempt(clientQuery);
console.log(
`[conn] Secure rsocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`,
);
const clientConnection: ClientConnection = { const clientConnection: ClientConnection = {
subscribeToEvents(subscriber: ConnectionStatusChange): void { subscribeToEvents(subscriber: ConnectionStatusChange): void {
@@ -168,10 +171,13 @@ class ServerRSocket extends ServerAdapter {
); );
client client
.then((client) => { .then((client) => {
console.log(
`[conn] Client created: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`,
);
resolvedClient = client; resolvedClient = client;
}) })
.catch((e) => { .catch((e) => {
console.error('Failed to resolve new client', e); console.error('[conn] Failed to resolve new client', e);
}); });
return { return {
@@ -184,7 +190,9 @@ class ServerRSocket extends ServerAdapter {
.then((client) => { .then((client) => {
client.onMessage(payload.data); client.onMessage(payload.data);
}) })
.catch((_) => {}); .catch((e) => {
console.error('Could not deliver message: ', e);
});
} }
}, },
}; };
@@ -221,7 +229,7 @@ class ServerRSocket extends ServerAdapter {
rawData = JSON.parse(payload.data); rawData = JSON.parse(payload.data);
} catch (err) { } catch (err) {
console.error( console.error(
`Invalid JSON: ${payload.data}`, `[conn] Invalid JSON: ${payload.data}`,
'clientMessage', 'clientMessage',
'server', 'server',
); );
@@ -263,7 +271,10 @@ class ServerRSocket extends ServerAdapter {
this._onHandleUntrustedMessage(clientQuery, rawData) this._onHandleUntrustedMessage(clientQuery, rawData)
.then((_) => {}) .then((_) => {})
.catch((err) => { .catch((err) => {
console.error('Unable to process CSR, failed with error.', err); console.error(
'[conn] Unable to process CSR, failed with error.',
err,
);
}); });
} }
}, },

View File

@@ -51,11 +51,16 @@ class ServerWebSocket extends ServerWebSocketBase {
const query = querystring.decode(message.url.split('?')[1]); const query = querystring.decode(message.url.split('?')[1]);
const clientQuery = this._parseClientQuery(query); const clientQuery = this._parseClientQuery(query);
if (!clientQuery) { if (!clientQuery) {
console.warn('Unable to extract the client query from the request URL.'); console.warn(
'[conn] Unable to extract the client query from the request URL.',
);
ws.close(); ws.close();
return; return;
} }
console.log(
`[conn] Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`,
);
this.listener.onConnectionAttempt(clientQuery); this.listener.onConnectionAttempt(clientQuery);
ws.on('message', async (message: any) => { ws.on('message', async (message: any) => {
@@ -78,10 +83,15 @@ class ServerWebSocket extends ServerWebSocketBase {
const query = querystring.decode(message.url.split('?')[1]); const query = querystring.decode(message.url.split('?')[1]);
const clientQuery = this._parseSecureClientQuery(query); const clientQuery = this._parseSecureClientQuery(query);
if (!clientQuery) { if (!clientQuery) {
console.warn('Unable to extract the client query from the request URL.'); console.warn(
'[conn] Unable to extract the client query from the request URL.',
);
ws.close(); ws.close();
return; return;
} }
console.log(
`[conn] Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`,
);
this.listener.onSecureConnectionAttempt(clientQuery); this.listener.onSecureConnectionAttempt(clientQuery);
const pendingRequests: Map< const pendingRequests: Map<
@@ -116,7 +126,14 @@ class ServerWebSocket extends ServerWebSocketBase {
clientQuery, clientQuery,
clientConnection, clientConnection,
); );
client.then((client) => (resolvedClient = client)).catch((_) => {}); 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) => { ws.on('message', (message: any) => {
let json: any | undefined; let json: any | undefined;

View File

@@ -72,7 +72,7 @@ abstract class ServerWebSocketBase extends ServerAdapter {
handleRequest.apply(self, [ws, message]); handleRequest.apply(self, [ws, message]);
}); });
rawServer.on('error', (_ws: WebSocket, error: any) => { rawServer.on('error', (_ws: WebSocket, error: any) => {
console.warn('Server found connection error: ' + error); console.warn('[conn] Server found connection error: ' + error);
reject(error); reject(error);
}); });

View File

@@ -27,9 +27,15 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
verifyClient(): ws.VerifyClientCallbackSync { verifyClient(): ws.VerifyClientCallbackSync {
return (info: {origin: string; req: IncomingMessage; secure: boolean}) => { return (info: {origin: string; req: IncomingMessage; secure: boolean}) => {
return constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES.some( const ok = constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES.some(
(validPrefix) => info.origin.startsWith(validPrefix), (validPrefix) => info.origin.startsWith(validPrefix),
); );
if (!ok) {
console.warn(
`[conn] Refused webSocket connection from ${info.origin} (secure: ${info.secure})`,
);
}
return ok;
}; };
} }
@@ -60,6 +66,9 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
os: 'MacOS', // TODO: not hardcoded! Use host device? os: 'MacOS', // TODO: not hardcoded! Use host device?
}; };
console.log(
`[conn] Local websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`,
);
this.listener.onConnectionAttempt(clientQuery); this.listener.onConnectionAttempt(clientQuery);
const cleanup = () => { const cleanup = () => {
@@ -104,6 +113,10 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
const extendedClientQuery = {...clientQuery, medium: 1}; const extendedClientQuery = {...clientQuery, medium: 1};
extendedClientQuery.sdk_version = plugins == null ? 4 : 1; extendedClientQuery.sdk_version = plugins == null ? 4 : 1;
console.log(
`[conn] Local websocket connection established: ${clientQuery.app} on ${clientQuery.device_id}.`,
);
let resolvedClient: Client | null = null; let resolvedClient: Client | null = null;
const client: Promise<Client> = this.listener.onConnectionCreated( const client: Promise<Client> = this.listener.onConnectionCreated(
extendedClientQuery, extendedClientQuery,
@@ -111,10 +124,16 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
); );
client client
.then((client) => { .then((client) => {
console.log(
`[conn] Client created: ${clientQuery.app} on ${clientQuery.device_id}.`,
);
resolvedClient = client; resolvedClient = client;
}) })
.catch((e) => { .catch((e) => {
console.error('Failed to connect client over webSocket', e); console.error(
'[conn] Failed to connect client over webSocket',
e,
);
}); });
clients[app] = client; clients[app] = client;
@@ -125,7 +144,7 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
parsed = JSON.parse(m.toString()); parsed = JSON.parse(m.toString());
} catch (error) { } catch (error) {
// Throws a SyntaxError exception if the string to parse is not valid JSON. // Throws a SyntaxError exception if the string to parse is not valid JSON.
console.log('Received message is not valid.', error); console.log('[conn] Received message is not valid.', error);
return; return;
} }
// non-null payload id means response to prev request, it's handled in connection // non-null payload id means response to prev request, it's handled in connection
@@ -159,7 +178,7 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
}); });
/** Error event from the existing client connection. */ /** Error event from the existing client connection. */
ws.on('error', (error) => { ws.on('error', (error) => {
console.warn('Server found connection error: ' + error); console.warn('[conn] Server found connection error: ' + error);
cleanup(); cleanup();
}); });
} }

View File

@@ -110,6 +110,10 @@ export default class CertificateProvider {
this.ensureServerCertExists(), this.ensureServerCertExists(),
'ensureServerCertExists', 'ensureServerCertExists',
); );
// make sure initialization failure is already logged
this.certificateSetup.catch((e) => {
console.error('Failed to find or generate certificates', e);
});
} }
this.config = config; this.config = config;
this.server = server; this.server = server;
@@ -128,10 +132,10 @@ export default class CertificateProvider {
certificate_zip: file, certificate_zip: file,
device_id: deviceID, device_id: deviceID,
}), }),
'Timed out uploading Flipper export.', 'Timed out uploading Flipper certificates to WWW.',
), ),
'uploadCertificates', 'uploadCertificates',
).catch((e) => console.error(`Failed to upload certificates due to ${e}`)); );
}; };
async processCertificateSigningRequest( async processCertificateSigningRequest(
@@ -148,10 +152,9 @@ export default class CertificateProvider {
const rootFolder = await promisify(tmp.dir)(); const rootFolder = await promisify(tmp.dir)();
const certFolder = rootFolder + '/FlipperCerts/'; const certFolder = rootFolder + '/FlipperCerts/';
const certsZipPath = rootFolder + '/certs.zip'; const certsZipPath = rootFolder + '/certs.zip';
return this.certificateSetup await this.certificateSetup;
.then((_) => this.getCACertificate()) const caCert = await this.getCACertificate();
.then((caCert) => await this.deployOrStageFileForMobileApp(
this.deployOrStageFileForMobileApp(
appDirectory, appDirectory,
deviceCAcertFile, deviceCAcertFile,
caCert, caCert,
@@ -159,11 +162,9 @@ export default class CertificateProvider {
os, os,
medium, medium,
certFolder, certFolder,
), );
) const clientCert = await this.generateClientCertificate(csr);
.then((_) => this.generateClientCertificate(csr)) await this.deployOrStageFileForMobileApp(
.then((clientCert) =>
this.deployOrStageFileForMobileApp(
appDirectory, appDirectory,
deviceClientCertFile, deviceClientCertFile,
clientCert, clientCert,
@@ -171,19 +172,12 @@ export default class CertificateProvider {
os, os,
medium, medium,
certFolder, certFolder,
), );
) const appName = await this.extractAppNameFromCSR(csr);
.then((_) => { const deviceId =
return this.extractAppNameFromCSR(csr); medium === 'FS_ACCESS'
}) ? await this.getTargetDeviceId(os, appName, appDirectory, csr)
.then((appName) => { : uuid();
if (medium === 'FS_ACCESS') {
return this.getTargetDeviceId(os, appName, appDirectory, csr);
} else {
return uuid();
}
})
.then(async (deviceId) => {
if (medium === 'WWW') { if (medium === 'WWW') {
const zipPromise = new Promise((resolve, reject) => { const zipPromise = new Promise((resolve, reject) => {
const output = fs.createWriteStream(certsZipPath); const output = fs.createWriteStream(certsZipPath);
@@ -212,7 +206,6 @@ export default class CertificateProvider {
return { return {
deviceId, deviceId,
}; };
});
} }
getTargetDeviceId( getTargetDeviceId(
@@ -276,50 +269,46 @@ export default class CertificateProvider {
medium: CertificateExchangeMedium, medium: CertificateExchangeMedium,
certFolder: string, certFolder: string,
): Promise<void> { ): Promise<void> {
const appNamePromise = this.extractAppNameFromCSR(csr);
if (medium === 'WWW') { if (medium === 'WWW') {
const certPathExists = await fs.pathExists(certFolder); const certPathExists = await fs.pathExists(certFolder);
if (!certPathExists) { if (!certPathExists) {
await fs.mkdir(certFolder); await fs.mkdir(certFolder);
} }
return fs.writeFile(certFolder + filename, contents).catch((e) => { try {
await fs.writeFile(certFolder + filename, contents);
return;
} catch (e) {
throw new Error( throw new Error(
`Failed to write ${filename} to temporary folder. Error: ${e}`, `Failed to write ${filename} to temporary folder. Error: ${e}`,
); );
}); }
} }
const appName = await this.extractAppNameFromCSR(csr);
if (os === 'Android') { if (os === 'Android') {
const deviceIdPromise = appNamePromise.then((app) => const deviceId = await this.getTargetAndroidDeviceId(
this.getTargetAndroidDeviceId(app, destination, csr), appName,
destination,
csr,
); );
return Promise.all([deviceIdPromise, appNamePromise, this.adb]).then( const adbClient = await this.adb;
([deviceId, appName, adbClient]) => await androidUtil.push(
androidUtil.push(
adbClient, adbClient,
deviceId, deviceId,
appName, appName,
destination + filename, destination + filename,
contents, contents,
),
); );
} } else if (os === 'iOS') {
if (os === 'iOS' || os === 'windows' || os == 'MacOS') { try {
return fs await fs.writeFile(destination + filename, contents);
.writeFile(destination + filename, contents) } catch (err) {
.catch(async (err) => {
if (os === 'iOS') {
// Writing directly to FS failed. It's probably a physical device. // Writing directly to FS failed. It's probably a physical device.
const relativePathInsideApp = const relativePathInsideApp =
this.getRelativePathInAppContainer(destination); this.getRelativePathInAppContainer(destination);
const appName = await appNamePromise; const udid = await this.getTargetiOSDeviceId(appName, destination, csr);
const udid = await this.getTargetiOSDeviceId( await this.pushFileToiOSDevice(
appName,
destination,
csr,
);
return await this.pushFileToiOSDevice(
udid, udid,
appName, appName,
relativePathInsideApp, relativePathInsideApp,
@@ -327,13 +316,9 @@ export default class CertificateProvider {
contents, contents,
); );
} }
throw new Error( } else {
`Invalid appDirectory recieved from ${os} device: ${destination}: ` + throw new Error(`Unsupported device OS for Certificate Exchange: ${os}`);
err.toString(),
);
});
} }
return Promise.reject(new Error(`Unsupported device os: ${os}`));
} }
private async pushFileToiOSDevice( private async pushFileToiOSDevice(
@@ -346,7 +331,7 @@ export default class CertificateProvider {
const dir = await tmpDir({unsafeCleanup: true}); const dir = await tmpDir({unsafeCleanup: true});
const filePath = path.resolve(dir, filename); const filePath = path.resolve(dir, filename);
await fs.writeFile(filePath, contents); await fs.writeFile(filePath, contents);
return await iosUtil.push( await iosUtil.push(
udid, udid,
filePath, filePath,
bundleId, bundleId,
@@ -360,11 +345,11 @@ export default class CertificateProvider {
deviceCsrFilePath: string, deviceCsrFilePath: string,
csr: string, csr: string,
): Promise<string> { ): Promise<string> {
const devices = await this.adb.then((client) => client.listDevices()); const devicesInAdb = await this.adb.then((client) => client.listDevices());
if (devices.length === 0) { if (devicesInAdb.length === 0) {
throw new Error('No Android devices found'); throw new Error('No Android devices found');
} }
const deviceMatchList = devices.map(async (device) => { const deviceMatchList = devicesInAdb.map(async (device) => {
try { try {
const result = await this.androidDeviceHasMatchingCSR( const result = await this.androidDeviceHasMatchingCSR(
deviceCsrFilePath, deviceCsrFilePath,
@@ -381,7 +366,7 @@ export default class CertificateProvider {
return {id: device.id, isMatch: false, foundCsr: null, error: e}; return {id: device.id, isMatch: false, foundCsr: null, error: e};
} }
}); });
return Promise.all(deviceMatchList).then((devices) => { const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) { if (matchingIds.length == 0) {
const erroredDevice = devices.find((d) => d.error); const erroredDevice = devices.find((d) => d.error);
@@ -402,12 +387,11 @@ export default class CertificateProvider {
} }
if (matchingIds.length > 1) { if (matchingIds.length > 1) {
console.warn( console.warn(
new Error('More than one matching device found for CSR'), new Error('[conn] More than one matching device found for CSR'),
csr, csr,
); );
} }
return matchingIds[0]; return matchingIds[0];
});
} }
private async getTargetiOSDeviceId( private async getTargetiOSDeviceId(
@@ -418,7 +402,7 @@ export default class CertificateProvider {
const matches = /\/Devices\/([^/]+)\//.exec(deviceCsrFilePath); const matches = /\/Devices\/([^/]+)\//.exec(deviceCsrFilePath);
if (matches && matches.length == 2) { if (matches && matches.length == 2) {
// It's a simulator, the deviceId is in the filepath. // It's a simulator, the deviceId is in the filepath.
return Promise.resolve(matches[1]); return matches[1];
} }
const targets = await iosUtil.targets( const targets = await iosUtil.targets(
this.config.idbPath, this.config.idbPath,
@@ -436,31 +420,30 @@ export default class CertificateProvider {
); );
return {id: target.udid, isMatch}; return {id: target.udid, isMatch};
}); });
return Promise.all(deviceMatchList).then((devices) => { const devices = await Promise.all(deviceMatchList);
const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id);
if (matchingIds.length == 0) { if (matchingIds.length == 0) {
throw new Error(`No matching device found for app: ${appName}`); throw new Error(`No matching device found for app: ${appName}`);
} }
if (matchingIds.length > 1) {
console.warn(`Multiple devices found for app: ${appName}`);
}
return matchingIds[0]; return matchingIds[0];
});
} }
private androidDeviceHasMatchingCSR( private async androidDeviceHasMatchingCSR(
directory: string, directory: string,
deviceId: string, deviceId: string,
processName: string, processName: string,
csr: string, csr: string,
): Promise<{isMatch: boolean; foundCsr: string}> { ): Promise<{isMatch: boolean; foundCsr: string}> {
return this.adb const adbClient = await this.adb;
.then((adbClient) => const deviceCsr = await androidUtil.pull(
androidUtil.pull(
adbClient, adbClient,
deviceId, deviceId,
processName, processName,
directory + csrFileName, directory + csrFileName,
), );
)
.then((deviceCsr) => {
// Santitize both of the string before comparation // Santitize both of the string before comparation
// The csr string extraction on client side return string in both way // The csr string extraction on client side return string in both way
const [sanitizedDeviceCsr, sanitizedClientCsr] = [ const [sanitizedDeviceCsr, sanitizedClientCsr] = [
@@ -469,7 +452,6 @@ export default class CertificateProvider {
].map((s) => this.santitizeString(s)); ].map((s) => this.santitizeString(s));
const isMatch = sanitizedDeviceCsr === sanitizedClientCsr; const isMatch = sanitizedDeviceCsr === sanitizedClientCsr;
return {isMatch: isMatch, foundCsr: sanitizedDeviceCsr}; return {isMatch: isMatch, foundCsr: sanitizedDeviceCsr};
});
} }
private async iOSDeviceHasMatchingCSR( private async iOSDeviceHasMatchingCSR(
@@ -553,30 +535,29 @@ export default class CertificateProvider {
private async checkCertIsValid(filename: string): Promise<void> { private async checkCertIsValid(filename: string): Promise<void> {
if (!(await fs.pathExists(filename))) { if (!(await fs.pathExists(filename))) {
return Promise.reject(new Error(`${filename} does not exist`)); throw new Error(`${filename} does not exist`);
} }
// openssl checkend is a nice feature but it only checks for certificates // openssl checkend is a nice feature but it only checks for certificates
// expiring in the future, not those that have already expired. // expiring in the future, not those that have already expired.
// So we need a separate check for certificates that have already expired // So we need a separate check for certificates that have already expired
// but since this involves parsing date outputs from openssl, which is less // but since this involves parsing date outputs from openssl, which is less
// reliable, keeping both checks for safety. // reliable, keeping both checks for safety.
return openssl('x509', { try {
await openssl('x509', {
checkend: minCertExpiryWindowSeconds, checkend: minCertExpiryWindowSeconds,
in: filename, in: filename,
}) });
.then(() => undefined) } catch (e) {
.catch((e) => { console.warn(
console.warn(`Certificate will expire soon: ${filename}`, logTag); `Checking if certificate expire soon: ${filename}`,
throw e; logTag,
}) e,
.then((_) => );
openssl('x509', { const endDateOutput = await openssl('x509', {
enddate: true, enddate: true,
in: filename, in: filename,
noout: true, noout: true,
}), });
)
.then((endDateOutput) => {
const dateString = endDateOutput.trim().split('=')[1].trim(); const dateString = endDateOutput.trim().split('=')[1].trim();
const expiryDate = Date.parse(dateString); const expiryDate = Date.parse(dateString);
if (isNaN(expiryDate)) { if (isNaN(expiryDate)) {
@@ -590,22 +571,21 @@ export default class CertificateProvider {
if (expiryDate <= Date.now() + minCertExpiryWindowSeconds * 1000) { if (expiryDate <= Date.now() + minCertExpiryWindowSeconds * 1000) {
throw new Error('Certificate has expired or will expire soon.'); throw new Error('Certificate has expired or will expire soon.');
} }
}); }
} }
private verifyServerCertWasIssuedByCA() { private async verifyServerCertWasIssuedByCA() {
const options: { const options: {
[key: string]: any; [key: string]: any;
} = {CAfile: caCert}; } = {CAfile: caCert};
options[serverCert] = false; options[serverCert] = false;
return openssl('verify', options).then((output) => { const output = await openssl('verify', options);
const verified = output.match(/[^:]+: OK/); const verified = output.match(/[^:]+: OK/);
if (!verified) { if (!verified) {
// This should never happen, but if it does, we need to notice so we can // This should never happen, but if it does, we need to notice so we can
// generate a valid one, or no clients will trust our server. // generate a valid one, or no clients will trust our server.
throw new Error('Current server cert was not issued by current CA'); throw new Error('Current server cert was not issued by current CA');
} }
});
} }
private async generateCertificateAuthority(): Promise<void> { private async generateCertificateAuthority(): Promise<void> {
@@ -613,21 +593,18 @@ export default class CertificateProvider {
await fs.mkdir(getFilePath('')); await fs.mkdir(getFilePath(''));
} }
console.log('Generating new CA', logTag); console.log('Generating new CA', logTag);
return openssl('genrsa', {out: caKey, '2048': false}) await openssl('genrsa', {out: caKey, '2048': false});
.then((_) => await openssl('req', {
openssl('req', {
new: true, new: true,
x509: true, x509: true,
subj: caSubject, subj: caSubject,
key: caKey, key: caKey,
out: caCert, out: caCert,
}), });
)
.then((_) => undefined);
} }
private async ensureServerCertExists(): Promise<void> { private async ensureServerCertExists(): Promise<void> {
const allExist = Promise.all([ const allExist = await Promise.all([
fs.pathExists(serverKey), fs.pathExists(serverKey),
fs.pathExists(serverCert), fs.pathExists(serverCert),
fs.pathExists(caCert), fs.pathExists(caCert),
@@ -636,27 +613,26 @@ export default class CertificateProvider {
return this.generateServerCertificate(); return this.generateServerCertificate();
} }
return this.checkCertIsValid(serverCert) try {
.then(() => this.verifyServerCertWasIssuedByCA()) await this.checkCertIsValid(serverCert);
.catch(() => this.generateServerCertificate()); await this.verifyServerCertWasIssuedByCA();
} catch (e) {
console.warn('Not all certs are valid, generating new ones', e);
await this.generateServerCertificate();
}
} }
private generateServerCertificate(): Promise<void> { private async generateServerCertificate(): Promise<void> {
return this.ensureCertificateAuthorityExists() await this.ensureCertificateAuthorityExists();
.then((_) => {
console.warn('Creating new server cert', logTag); console.warn('Creating new server cert', logTag);
}) await openssl('genrsa', {out: serverKey, '2048': false});
.then((_) => openssl('genrsa', {out: serverKey, '2048': false})) await openssl('req', {
.then((_) =>
openssl('req', {
new: true, new: true,
key: serverKey, key: serverKey,
out: serverCsr, out: serverCsr,
subj: serverSubject, subj: serverSubject,
}), });
) await openssl('x509', {
.then((_) =>
openssl('x509', {
req: true, req: true,
in: serverCsr, in: serverCsr,
CA: caCert, CA: caCert,
@@ -664,9 +640,7 @@ export default class CertificateProvider {
CAcreateserial: true, CAcreateserial: true,
CAserial: serverSrl, CAserial: serverSrl,
out: serverCert, out: serverCert,
}), });
)
.then((_) => undefined);
} }
private async writeToTempFile(content: string): Promise<string> { private async writeToTempFile(content: string): Promise<string> {

View File

@@ -30,7 +30,7 @@ export type CrashLog = {
export function devicePlugin(client: DevicePluginClient) { export function devicePlugin(client: DevicePluginClient) {
let notificationID = -1; let notificationID = -1;
let watcher: Promise<FSWatcher | undefined>; let watcher: Promise<FSWatcher | undefined> | undefined = undefined;
const crashes = createState<Crash[]>([], {persist: 'crashes'}); const crashes = createState<Crash[]>([], {persist: 'crashes'});
const selectedCrash = createState<string | undefined>(); const selectedCrash = createState<string | undefined>();
@@ -71,7 +71,7 @@ export function devicePlugin(client: DevicePluginClient) {
client.onDestroy(() => { client.onDestroy(() => {
watcher watcher
.then((watcher) => watcher?.close()) ?.then((watcher) => watcher?.close())
.catch((e) => .catch((e) =>
console.error( console.error(
'[crash_reporter] FSWatcher failed resoving on destroy:', '[crash_reporter] FSWatcher failed resoving on destroy:',