diff --git a/desktop/flipper-server-core/src/comms/ServerController.tsx b/desktop/flipper-server-core/src/comms/ServerController.tsx index 1b04d653c..ca17f9a59 100644 --- a/desktop/flipper-server-core/src/comms/ServerController.tsx +++ b/desktop/flipper-server-core/src/comms/ServerController.tsx @@ -45,6 +45,7 @@ import { extractAppNameFromCSR, loadSecureServerConfig, } from '../utils/certificateUtils'; +import DesktopCertificateProvider from '../devices/desktop/DesktopCertificateProvider'; type ClientTimestampTracker = { insecureStart?: number; @@ -83,7 +84,6 @@ class ServerController extends EventEmitter implements ServerEventsListener { altInsecureServer: ServerAdapter | null = null; browserServer: ServerAdapter | null = null; - certificateProvider: CertificateProvider; connectionTracker: ConnectionTracker; flipperServer: FlipperServerImpl; @@ -93,7 +93,6 @@ class ServerController extends EventEmitter implements ServerEventsListener { constructor(flipperServer: FlipperServerImpl) { super(); this.flipperServer = flipperServer; - this.certificateProvider = new CertificateProvider(this); this.connectionTracker = new ConnectionTracker(this.logger); } @@ -278,9 +277,35 @@ class ServerController extends EventEmitter implements ServerEventsListener { appDirectory: string, medium: CertificateExchangeMedium, ): Promise<{deviceId: string}> { + let certificateProvider: CertificateProvider; + switch (clientQuery.os) { + case 'Android': { + certificateProvider = this.flipperServer.android.certificateProvider; + break; + } + case 'iOS': { + certificateProvider = this.flipperServer.ios.certificateProvider; + break; + } + // Used by Spark AR studio (search for SkylightFlipperClient) + // See D30992087 + case 'MacOS': + case 'Windows': { + certificateProvider = new DesktopCertificateProvider( + this.flipperServer.keytarManager, + ); + break; + } + default: { + throw new Error( + `ServerController.onProcessCSR -> os ${clientQuery.os} does not support certificate exchange.`, + ); + } + } + return new Promise((resolve, reject) => { reportPlatformFailures( - this.certificateProvider.processCertificateSigningRequest( + certificateProvider.processCertificateSigningRequest( unsanitizedCSR, clientQuery.os, appDirectory, @@ -350,8 +375,7 @@ class ServerController extends EventEmitter implements ServerEventsListener { const app_name = await extractAppNameFromCSR(csr); // TODO: allocate new object, kept now as is to keep changes minimal (query as any).device_id = - await this.certificateProvider.getTargetDeviceId( - query.os, + await this.flipperServer.android.certificateProvider.getTargetDeviceId( app_name, csr_path, csr, diff --git a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx new file mode 100644 index 000000000..45eb7acdc --- /dev/null +++ b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx @@ -0,0 +1,116 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 CertificateProvider from '../../utils/CertificateProvider'; +import {Client} from 'adbkit'; +import {KeytarManager} from '../../utils/keytar'; +import * as androidUtil from './androidContainerUtility'; +import {csrFileName} from '../../utils/certificateUtils'; + +const logTag = 'AndroidCertificateProvider'; + +export default class AndroidCertificateProvider extends CertificateProvider { + constructor(keytarManager: KeytarManager, private adb: Client) { + super(keytarManager); + } + + async getTargetDeviceId( + appName: string, + appDirectory: string, + csr: string, + ): Promise { + const devicesInAdb = await this.adb.listDevices(); + if (devicesInAdb.length === 0) { + throw new Error('No Android devices found'); + } + const deviceMatchList = devicesInAdb.map(async (device) => { + try { + const result = await this.androidDeviceHasMatchingCSR( + appDirectory, + device.id, + appName, + csr, + ); + return {id: device.id, ...result, error: null}; + } catch (e) { + console.warn( + `Unable to check for matching CSR in ${device.id}:${appName}`, + logTag, + e, + ); + return {id: device.id, isMatch: false, foundCsr: null, error: e}; + } + }); + const devices = await Promise.all(deviceMatchList); + const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); + if (matchingIds.length == 0) { + const erroredDevice = devices.find((d) => d.error); + if (erroredDevice) { + throw erroredDevice.error; + } + const foundCsrs = devices + .filter((d) => d.foundCsr !== null) + .map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null')); + console.warn(`Looking for CSR (url encoded): + + ${encodeURI(this.santitizeString(csr))} + + Found these: + + ${foundCsrs.join('\n\n')}`); + throw new Error(`No matching device found for app: ${appName}`); + } + if (matchingIds.length > 1) { + console.warn( + new Error('[conn] More than one matching device found for CSR'), + csr, + ); + } + return matchingIds[0]; + } + + protected async handleFSBasedDeploy( + destination: string, + filename: string, + contents: string, + csr: string, + appName: string, + ) { + const deviceId = await this.getTargetDeviceId(appName, destination, csr); + await androidUtil.push( + this.adb, + deviceId, + appName, + destination + filename, + contents, + ); + } + + private async androidDeviceHasMatchingCSR( + directory: string, + deviceId: string, + processName: string, + csr: string, + ): Promise<{isMatch: boolean; foundCsr: string}> { + const deviceCsr = await androidUtil.pull( + this.adb, + deviceId, + processName, + directory + csrFileName, + ); + // Santitize both of the string before comparation + // The csr string extraction on client side return string in both way + const [sanitizedDeviceCsr, sanitizedClientCsr] = [ + deviceCsr.toString(), + csr, + ].map((s) => this.santitizeString(s)); + const isMatch = sanitizedDeviceCsr === sanitizedClientCsr; + return {isMatch: isMatch, foundCsr: sanitizedDeviceCsr}; + } +} diff --git a/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx b/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx index 92e014021..0baa641b7 100644 --- a/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx +++ b/desktop/flipper-server-core/src/devices/android/androidDeviceManager.tsx @@ -19,10 +19,24 @@ import { getServerPortsConfig, getFlipperServerConfig, } from '../../FlipperServerConfig'; +import AndroidCertificateProvider from './AndroidCertificateProvider'; +import {assertNotNull} from '../../comms/Utilities'; export class AndroidDeviceManager { + private adbClient?: ADBClient; constructor(public flipperServer: FlipperServerImpl) {} + public get certificateProvider() { + assertNotNull( + this.adbClient, + 'AndroidDeviceManager.certificateProvider -> missing adbClient', + ); + return new AndroidCertificateProvider( + this.flipperServer.keytarManager, + this.adbClient, + ); + } + private createDevice( adbClient: ADBClient, device: Device, @@ -177,6 +191,8 @@ export class AndroidDeviceManager { ); } + this.adbClient = client; + client .trackDevices() .then((tracker) => { diff --git a/desktop/flipper-server-core/src/devices/desktop/DesktopCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/desktop/DesktopCertificateProvider.tsx new file mode 100644 index 000000000..642e7a993 --- /dev/null +++ b/desktop/flipper-server-core/src/devices/desktop/DesktopCertificateProvider.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 CertificateProvider from '../../utils/CertificateProvider'; +import fs from 'fs-extra'; + +export default class DesktopCertificateProvider extends CertificateProvider { + async getTargetDeviceId(): Promise { + // TODO: Could we use some real device serial? Currently, '' corresponds to a local device. + // Whats if some app connects from a remote device? + // What if two apps connect from several different remote devices? + return ''; + } + + protected async handleFSBasedDeploy( + destination: string, + filename: string, + contents: string, + ) { + await fs.writeFile(destination + filename, contents); + } +} diff --git a/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx new file mode 100644 index 000000000..112c47ef6 --- /dev/null +++ b/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx @@ -0,0 +1,189 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 {IdbConfig} from './idbConfig'; +import {KeytarManager} from '../../utils/keytar'; +import CertificateProvider from '../../utils/CertificateProvider'; +import iosUtil from './iOSContainerUtility'; +import fs from 'fs-extra'; +import {promisify} from 'util'; +import tmp, {DirOptions} from 'tmp'; +import {csrFileName} from '../../utils/certificateUtils'; +import path from 'path'; + +const tmpDir = promisify(tmp.dir) as (options?: DirOptions) => Promise; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export default class iOSCertificateProvider extends CertificateProvider { + constructor(keytarManager: KeytarManager, private idbConfig: IdbConfig) { + super(keytarManager); + } + + async getTargetDeviceId( + appName: string, + appDirectory: string, + csr: string, + ): Promise { + const matches = /\/Devices\/([^/]+)\//.exec(appName); + if (matches && matches.length == 2) { + // It's a simulator, the deviceId is in the filepath. + return matches[1]; + } + const targets = await iosUtil.targets( + this.idbConfig.idbPath, + this.idbConfig.enablePhysicalIOS, + ); + if (targets.length === 0) { + throw new Error('No iOS devices found'); + } + const deviceMatchList = targets.map(async (target) => { + const isMatch = await this.iOSDeviceHasMatchingCSR( + appDirectory, + target.udid, + appName, + csr, + ); + return {id: target.udid, isMatch}; + }); + const devices = await Promise.all(deviceMatchList); + const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); + if (matchingIds.length == 0) { + 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]; + } + + protected async handleFSBasedDeploy( + destination: string, + filename: string, + contents: string, + csr: string, + appName: string, + ) { + try { + await fs.writeFile(destination + filename, contents); + } catch (err) { + // Writing directly to FS failed. It's probably a physical device. + const relativePathInsideApp = + this.getRelativePathInAppContainer(destination); + const udid = await this.getTargetDeviceId(appName, destination, csr); + await this.pushFileToiOSDevice( + udid, + appName, + relativePathInsideApp, + filename, + contents, + ); + } + } + + private getRelativePathInAppContainer(absolutePath: string) { + const matches = /Application\/[^/]+\/(.*)/.exec(absolutePath); + if (matches && matches.length === 2) { + return matches[1]; + } + throw new Error("Path didn't match expected pattern: " + absolutePath); + } + + private async pushFileToiOSDevice( + udid: string, + bundleId: string, + destination: string, + filename: string, + contents: string, + ): Promise { + const dir = await tmpDir({unsafeCleanup: true}); + const filePath = path.resolve(dir, filename); + await fs.writeFile(filePath, contents); + + await iosUtil.push( + udid, + filePath, + bundleId, + destination, + this.idbConfig.idbPath, + ); + } + + private async iOSDeviceHasMatchingCSR( + directory: string, + deviceId: string, + bundleId: string, + csr: string, + ): Promise { + const originalFile = this.getRelativePathInAppContainer( + path.resolve(directory, csrFileName), + ); + const dir = await tmpDir({unsafeCleanup: true}); + + // Workaround for idb weirdness + // Originally started at D27590885 + // Re-appared at https://github.com/facebook/flipper/issues/3009 + // + // People reported various workarounds. None of them worked consistently for everyone. + // Usually, the workarounds included re-building idb from source or re-installing it. + // + // The only more or less reasonable explanation I was able to find is that the final behavior depends on whether the idb_companion is local or not. + // + // This is how idb_companion sets its locality + // https://github.com/facebook/idb/blob/main/idb_companion/Server/FBIDBServiceHandler.mm#L1507 + // idb sends a connection request and provides a file path to a temporary file. idb_companion checks if it can access that file. + // + // So when it is "local", the pulled filed is written directly to the destination path + // https://github.com/facebook/idb/blob/main/idb/grpc/client.py#L698 + // So it is expected that the destination path ends with a file to write to. + // However, if the companion is remote, then we seem to get here https://github.com/facebook/idb/blob/71791652efa2d5e6f692cb8985ff0d26b69bf08f/idb/common/tar.py#L232 + // Where we create a tree of directories and write the file stream there. + // + // So the only explanation I could come up with is that somehow, by re-installing idb and playing with the env, people could affect the locality of the idb_companion. + // + // The ultimate workaround is to try pulling the cert file without the cert name attached first, if it fails, try to append it. + try { + await iosUtil.pull( + deviceId, + originalFile, + bundleId, + dir, + this.idbConfig.idbPath, + ); + } catch (e) { + console.warn( + 'Original idb pull failed. Most likely it is a physical device that requires us to handle the dest path dirrently. Forcing a re-try with the updated dest path. See D32106952 for details. Original error:', + e, + ); + await iosUtil.pull( + deviceId, + originalFile, + bundleId, + path.join(dir, csrFileName), + this.idbConfig.idbPath, + ); + console.info( + 'Subsequent idb pull succeeded. Nevermind previous wranings.', + ); + } + + const items = await fs.readdir(dir); + if (items.length > 1) { + throw new Error('Conflict in temp dir'); + } + if (items.length === 0) { + throw new Error('Failed to pull CSR from device'); + } + const fileName = items[0]; + const copiedFile = path.resolve(dir, fileName); + console.debug('Trying to read CSR from', copiedFile); + const data = await fs.readFile(copiedFile); + const csrFromDevice = this.santitizeString(data.toString()); + return csrFromDevice === this.santitizeString(csr); + } +} diff --git a/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx b/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx index 6040888bf..d93b6d596 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx @@ -23,6 +23,8 @@ import { import {FlipperServerImpl} from '../../FlipperServerImpl'; import {getFlipperServerConfig} from '../../FlipperServerConfig'; import {IdbConfig, setIdbConfig} from './idbConfig'; +import {assertNotNull} from '../../comms/Utilities'; +import iOSCertificateProvider from './iOSCertificateProvider'; export class IOSDeviceManager { private portForwarders: Array = []; @@ -39,6 +41,17 @@ export class IOSDeviceManager { constructor(private flipperServer: FlipperServerImpl) {} + public get certificateProvider() { + assertNotNull( + this.idbConfig, + 'IOSDeviceManager.certificateProvider -> missing idbConfig', + ); + return new iOSCertificateProvider( + this.flipperServer.keytarManager, + this.idbConfig, + ); + } + private forwardPort(port: number, multiplexChannelPort: number) { const child = childProcess.execFile( this.portforwardingClient, diff --git a/desktop/flipper-server-core/src/utils/CertificateProvider.tsx b/desktop/flipper-server-core/src/utils/CertificateProvider.tsx index 7a770555a..79bdd8427 100644 --- a/desktop/flipper-server-core/src/utils/CertificateProvider.tsx +++ b/desktop/flipper-server-core/src/utils/CertificateProvider.tsx @@ -7,24 +7,15 @@ * @format */ -import ServerController from '../comms/ServerController'; import {promisify} from 'util'; import fs from 'fs-extra'; - -import path from 'path'; -import tmp, {DirOptions} from 'tmp'; -import iosUtil from '../devices/ios/iOSContainerUtility'; +import tmp from 'tmp'; import {reportPlatformFailures} from 'flipper-common'; -import {getAdbClient} from '../devices/android/adbClient'; -import * as androidUtil from '../devices/android/androidContainerUtility'; import archiver from 'archiver'; import {timeout} from 'flipper-common'; import {v4 as uuid} from 'uuid'; import {internGraphPOSTAPIRequest} from '../fb-stubs/internRequests'; -import {getIdbConfig} from '../devices/ios/idbConfig'; -import {assertNotNull} from '../comms/Utilities'; import { - csrFileName, deviceCAcertFile, deviceClientCertFile, ensureOpenSSLIsAvailable, @@ -32,39 +23,12 @@ import { generateClientCertificate, getCACertificate, } from './certificateUtils'; -import {SERVICE_FLIPPER} from './keytar'; +import {KeytarManager, SERVICE_FLIPPER} from './keytar'; export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW' | 'NONE'; -const tmpDir = promisify(tmp.dir) as (options?: DirOptions) => Promise; - -const logTag = 'CertificateProvider'; - -/* - * This class is responsible for generating and deploying server and client - * certificates to allow for secure communication between Flipper and apps. - * It takes a Certificate Signing Request which was generated by the app, - * using the app's public/private keypair. - * With this CSR it uses the Flipper CA to sign a client certificate which it - * deploys securely to the app. - * It also deploys the Flipper CA cert to the app. - * The app can trust a server if and only if it has a certificate signed by the - * Flipper CA. - */ -export default class CertificateProvider { - private server: ServerController; - - constructor(server: ServerController) { - this.server = server; - } - - private get adb() { - return getAdbClient(); - } - - private get idbConfig() { - return getIdbConfig(); - } +export default abstract class CertificateProvider { + constructor(private readonly keytarManager: KeytarManager) {} private uploadFiles = async ( zipPath: string, @@ -85,9 +49,7 @@ export default class CertificateProvider { }, }, {timeout: 5 * 60 * 1000}, - await this.server.flipperServer.keytarManager.retrieveToken( - SERVICE_FLIPPER, - ), + await this.keytarManager.retrieveToken(SERVICE_FLIPPER), ).then(() => {}), 'Timed out uploading Flipper certificates to WWW.', ), @@ -110,29 +72,27 @@ export default class CertificateProvider { const certFolder = rootFolder + '/FlipperCerts/'; const certsZipPath = rootFolder + '/certs.zip'; const caCert = await getCACertificate(); - await this.deployOrStageFileForMobileApp( + await this.deployOrStageFileForDevice( appDirectory, deviceCAcertFile, caCert, csr, - os, medium, certFolder, ); const clientCert = await generateClientCertificate(csr); - await this.deployOrStageFileForMobileApp( + await this.deployOrStageFileForDevice( appDirectory, deviceClientCertFile, clientCert, csr, - os, medium, certFolder, ); const appName = await extractAppNameFromCSR(csr); const deviceId = medium === 'FS_ACCESS' - ? await this.getTargetDeviceId(os, appName, appDirectory, csr) + ? await this.getTargetDeviceId(appName, appDirectory, csr) : uuid(); if (medium === 'WWW') { const zipPromise = new Promise((resolve, reject) => { @@ -164,36 +124,17 @@ export default class CertificateProvider { }; } - getTargetDeviceId( - os: string, - appName: string, - appDirectory: string, - csr: string, - ): Promise { - if (os === 'Android') { - return this.getTargetAndroidDeviceId(appName, appDirectory, csr); - } else if (os === 'iOS') { - return this.getTargetiOSDeviceId(appName, appDirectory, csr); - } else if (os == 'MacOS') { - return Promise.resolve(''); - } - return Promise.resolve('unknown'); - } + abstract getTargetDeviceId( + _appName: string, + _appDirectory: string, + _csr: string, + ): Promise; - private getRelativePathInAppContainer(absolutePath: string) { - const matches = /Application\/[^/]+\/(.*)/.exec(absolutePath); - if (matches && matches.length === 2) { - return matches[1]; - } - throw new Error("Path didn't match expected pattern: " + absolutePath); - } - - private async deployOrStageFileForMobileApp( + private async deployOrStageFileForDevice( destination: string, filename: string, contents: string, csr: string, - os: string, medium: CertificateExchangeMedium, certFolder: string, ): Promise { @@ -213,265 +154,18 @@ export default class CertificateProvider { } const appName = await extractAppNameFromCSR(csr); - - if (os === 'Android') { - assertNotNull(this.adb); - - const deviceId = await this.getTargetAndroidDeviceId( - appName, - destination, - csr, - ); - await androidUtil.push( - this.adb, - deviceId, - appName, - destination + filename, - contents, - ); - } else if ( - os === 'iOS' || - os === 'windows' || - os == 'MacOS' /* Used by Spark AR?! */ - ) { - try { - await fs.writeFile(destination + filename, contents); - } catch (err) { - // Writing directly to FS failed. It's probably a physical device. - const relativePathInsideApp = - this.getRelativePathInAppContainer(destination); - const udid = await this.getTargetiOSDeviceId(appName, destination, csr); - await this.pushFileToiOSDevice( - udid, - appName, - relativePathInsideApp, - filename, - contents, - ); - } - } else { - throw new Error(`Unsupported device OS for Certificate Exchange: ${os}`); - } + this.handleFSBasedDeploy(destination, filename, contents, csr, appName); } - private async pushFileToiOSDevice( - udid: string, - bundleId: string, - destination: string, - filename: string, - contents: string, - ): Promise { - assertNotNull(this.idbConfig); + protected abstract handleFSBasedDeploy( + _destination: string, + _filename: string, + _contents: string, + _csr: string, + _appName: string, + ): Promise; - const dir = await tmpDir({unsafeCleanup: true}); - const filePath = path.resolve(dir, filename); - await fs.writeFile(filePath, contents); - - await iosUtil.push( - udid, - filePath, - bundleId, - destination, - this.idbConfig.idbPath, - ); - } - - private async getTargetAndroidDeviceId( - appName: string, - deviceCsrFilePath: string, - csr: string, - ): Promise { - assertNotNull(this.adb); - - const devicesInAdb = await this.adb.listDevices(); - if (devicesInAdb.length === 0) { - throw new Error('No Android devices found'); - } - const deviceMatchList = devicesInAdb.map(async (device) => { - try { - const result = await this.androidDeviceHasMatchingCSR( - deviceCsrFilePath, - device.id, - appName, - csr, - ); - return {id: device.id, ...result, error: null}; - } catch (e) { - console.warn( - `Unable to check for matching CSR in ${device.id}:${appName}`, - logTag, - e, - ); - return {id: device.id, isMatch: false, foundCsr: null, error: e}; - } - }); - const devices = await Promise.all(deviceMatchList); - const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); - if (matchingIds.length == 0) { - const erroredDevice = devices.find((d) => d.error); - if (erroredDevice) { - throw erroredDevice.error; - } - const foundCsrs = devices - .filter((d) => d.foundCsr !== null) - .map((d) => (d.foundCsr ? encodeURI(d.foundCsr) : 'null')); - console.warn(`Looking for CSR (url encoded): - - ${encodeURI(this.santitizeString(csr))} - - Found these: - - ${foundCsrs.join('\n\n')}`); - throw new Error(`No matching device found for app: ${appName}`); - } - if (matchingIds.length > 1) { - console.warn( - new Error('[conn] More than one matching device found for CSR'), - csr, - ); - } - return matchingIds[0]; - } - - private async getTargetiOSDeviceId( - appName: string, - deviceCsrFilePath: string, - csr: string, - ): Promise { - assertNotNull(this.idbConfig); - - const matches = /\/Devices\/([^/]+)\//.exec(deviceCsrFilePath); - if (matches && matches.length == 2) { - // It's a simulator, the deviceId is in the filepath. - return matches[1]; - } - const targets = await iosUtil.targets( - this.idbConfig.idbPath, - this.idbConfig.enablePhysicalIOS, - ); - if (targets.length === 0) { - throw new Error('No iOS devices found'); - } - const deviceMatchList = targets.map(async (target) => { - const isMatch = await this.iOSDeviceHasMatchingCSR( - deviceCsrFilePath, - target.udid, - appName, - csr, - ); - return {id: target.udid, isMatch}; - }); - const devices = await Promise.all(deviceMatchList); - const matchingIds = devices.filter((m) => m.isMatch).map((m) => m.id); - if (matchingIds.length == 0) { - 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]; - } - - private async androidDeviceHasMatchingCSR( - directory: string, - deviceId: string, - processName: string, - csr: string, - ): Promise<{isMatch: boolean; foundCsr: string}> { - assertNotNull(this.adb); - - const deviceCsr = await androidUtil.pull( - this.adb, - deviceId, - processName, - directory + csrFileName, - ); - // Santitize both of the string before comparation - // The csr string extraction on client side return string in both way - const [sanitizedDeviceCsr, sanitizedClientCsr] = [ - deviceCsr.toString(), - csr, - ].map((s) => this.santitizeString(s)); - const isMatch = sanitizedDeviceCsr === sanitizedClientCsr; - return {isMatch: isMatch, foundCsr: sanitizedDeviceCsr}; - } - - private async iOSDeviceHasMatchingCSR( - directory: string, - deviceId: string, - bundleId: string, - csr: string, - ): Promise { - assertNotNull(this.idbConfig); - - const originalFile = this.getRelativePathInAppContainer( - path.resolve(directory, csrFileName), - ); - const dir = await tmpDir({unsafeCleanup: true}); - - // Workaround for idb weirdness - // Originally started at D27590885 - // Re-appared at https://github.com/facebook/flipper/issues/3009 - // - // People reported various workarounds. None of them worked consistently for everyone. - // Usually, the workarounds included re-building idb from source or re-installing it. - // - // The only more or less reasonable explanation I was able to find is that the final behavior depends on whether the idb_companion is local or not. - // - // This is how idb_companion sets its locality - // https://github.com/facebook/idb/blob/main/idb_companion/Server/FBIDBServiceHandler.mm#L1507 - // idb sends a connection request and provides a file path to a temporary file. idb_companion checks if it can access that file. - // - // So when it is "local", the pulled filed is written directly to the destination path - // https://github.com/facebook/idb/blob/main/idb/grpc/client.py#L698 - // So it is expected that the destination path ends with a file to write to. - // However, if the companion is remote, then we seem to get here https://github.com/facebook/idb/blob/71791652efa2d5e6f692cb8985ff0d26b69bf08f/idb/common/tar.py#L232 - // Where we create a tree of directories and write the file stream there. - // - // So the only explanation I could come up with is that somehow, by re-installing idb and playing with the env, people could affect the locality of the idb_companion. - // - // The ultimate workaround is to try pulling the cert file without the cert name attached first, if it fails, try to append it. - try { - await iosUtil.pull( - deviceId, - originalFile, - bundleId, - dir, - this.idbConfig.idbPath, - ); - } catch (e) { - console.warn( - 'Original idb pull failed. Most likely it is a physical device that requires us to handle the dest path dirrently. Forcing a re-try with the updated dest path. See D32106952 for details. Original error:', - e, - ); - await iosUtil.pull( - deviceId, - originalFile, - bundleId, - path.join(dir, csrFileName), - this.idbConfig.idbPath, - ); - console.info( - 'Subsequent idb pull succeeded. Nevermind previous wranings.', - ); - } - - const items = await fs.readdir(dir); - if (items.length > 1) { - throw new Error('Conflict in temp dir'); - } - if (items.length === 0) { - throw new Error('Failed to pull CSR from device'); - } - const fileName = items[0]; - const copiedFile = path.resolve(dir, fileName); - console.debug('Trying to read CSR from', copiedFile); - const data = await fs.readFile(copiedFile); - const csrFromDevice = this.santitizeString(data.toString()); - return csrFromDevice === this.santitizeString(csr); - } - - private santitizeString(csrString: string): string { + protected santitizeString(csrString: string): string { return csrString.replace(/\r/g, '').trim(); } }