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:
committed by
Facebook GitHub Bot
parent
0816f73d07
commit
d8f77db632
@@ -138,10 +138,11 @@ abstract class ServerAdapter {
|
||||
async _onHandleUntrustedMessage(
|
||||
clientQuery: ClientQuery,
|
||||
rawData: any,
|
||||
): Promise<any> {
|
||||
): Promise<string | undefined> {
|
||||
// OSS's older Client SDK might not send medium information.
|
||||
// This is not an issue for internal FB users, as Flipper release
|
||||
// is insync with client SDK through launcher.
|
||||
|
||||
const message: {
|
||||
method: 'signCertificate';
|
||||
csr: string;
|
||||
@@ -149,24 +150,37 @@ abstract class ServerAdapter {
|
||||
medium: number | undefined;
|
||||
} = rawData;
|
||||
|
||||
console.log(
|
||||
`[conn] Connection attempt: ${clientQuery.app} on ${clientQuery.device}, medium: ${message.medium}, cert: ${message.destination}`,
|
||||
clientQuery,
|
||||
rawData,
|
||||
);
|
||||
|
||||
if (message.method === 'signCertificate') {
|
||||
console.debug('CSR received from device', 'server');
|
||||
|
||||
const {csr, destination, medium} = message;
|
||||
|
||||
console.log(
|
||||
`[conn] Starting certificate exchange: ${clientQuery.app} on ${clientQuery.device}`,
|
||||
);
|
||||
const result = await this.listener.onProcessCSR(
|
||||
csr,
|
||||
clientQuery,
|
||||
destination,
|
||||
transformCertificateExchangeMediumToType(medium),
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[conn] Exchanged certificate: ${clientQuery.app} on ${clientQuery.device_id}`,
|
||||
);
|
||||
const response = JSON.stringify({
|
||||
deviceId: result.deviceId,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
|
||||
const {app, os, device, device_id, sdk_version, csr, csr_path, medium} =
|
||||
clientQuery;
|
||||
const transformedMedium = transformCertificateExchangeMediumToType(medium);
|
||||
console.log(
|
||||
`[conn] Connection established: ${app} on ${device_id}. Medium ${medium}. CSR: ${csr_path}`,
|
||||
);
|
||||
return this.addConnection(
|
||||
clientConnection,
|
||||
{
|
||||
@@ -293,6 +296,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
|
||||
csr_path,
|
||||
csr,
|
||||
);
|
||||
console.log(
|
||||
`[conn] Detected ${app_name} on ${query.device_id} in certificate`,
|
||||
);
|
||||
}
|
||||
|
||||
query.app = appNameWithUpdateHint(query);
|
||||
@@ -303,8 +309,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
|
||||
device: query.device,
|
||||
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 =
|
||||
getDeviceBySerial(this.store.getState(), query.device_id) ??
|
||||
(await findDeviceForConnection(this.store, query.app, query.device_id));
|
||||
@@ -324,6 +331,10 @@ class ServerController extends EventEmitter implements ServerEventsListener {
|
||||
connection: connection,
|
||||
};
|
||||
|
||||
console.log(
|
||||
`[conn] Initializing client ${query.app} on ${query.device_id}...`,
|
||||
);
|
||||
|
||||
await client.init();
|
||||
|
||||
connection.subscribeToEvents((status: ConnectionStatus) => {
|
||||
@@ -336,7 +347,7 @@ class ServerController extends EventEmitter implements ServerEventsListener {
|
||||
});
|
||||
|
||||
console.debug(
|
||||
`Device client initialized: ${id}. Supported plugins: ${Array.from(
|
||||
`[conn] Device client initialized: ${id}. Supported plugins: ${Array.from(
|
||||
client.plugins,
|
||||
).join(', ')}`,
|
||||
'server',
|
||||
@@ -382,6 +393,9 @@ class ServerController extends EventEmitter implements ServerEventsListener {
|
||||
removeConnection(id: string) {
|
||||
const info = this.connections.get(id);
|
||||
if (info) {
|
||||
console.log(
|
||||
`[conn] Disconnected: ${info.client.query.app} on ${info.client.query.device_id}.`,
|
||||
);
|
||||
info.client.disconnect();
|
||||
this.connections.delete(id);
|
||||
this.emit('clients-change');
|
||||
@@ -412,7 +426,7 @@ class ConnectionTracker {
|
||||
this.connectionAttempts.set(key, entry);
|
||||
if (entry.length >= this.connectionProblemThreshold) {
|
||||
console.error(
|
||||
`Connection loop detected with ${key}. Connected ${
|
||||
`[conn] Connection loop detected with ${key}. Connected ${
|
||||
this.connectionProblemThreshold
|
||||
} times within ${this.timeWindowMillis / 1000}s.`,
|
||||
'server',
|
||||
@@ -444,7 +458,10 @@ async function findDeviceForConnection(
|
||||
const timeout = setTimeout(() => {
|
||||
unsubscribe();
|
||||
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);
|
||||
}, 15000);
|
||||
unsubscribe = sideEffect(
|
||||
@@ -460,6 +477,7 @@ async function findDeviceForConnection(
|
||||
(device) => device.serial === serial,
|
||||
);
|
||||
if (matchingDevice) {
|
||||
console.log(`[conn] Found device for: ${clientId} on ${serial}.`);
|
||||
clearTimeout(timeout);
|
||||
resolve(matchingDevice);
|
||||
unsubscribe();
|
||||
|
||||
@@ -109,6 +109,9 @@ class ServerRSocket extends ServerAdapter {
|
||||
|
||||
const clientQuery: SecureClientQuery = JSON.parse(payload.data);
|
||||
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 = {
|
||||
subscribeToEvents(subscriber: ConnectionStatusChange): void {
|
||||
@@ -168,10 +171,13 @@ class ServerRSocket extends ServerAdapter {
|
||||
);
|
||||
client
|
||||
.then((client) => {
|
||||
console.log(
|
||||
`[conn] Client created: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`,
|
||||
);
|
||||
resolvedClient = client;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to resolve new client', e);
|
||||
console.error('[conn] Failed to resolve new client', e);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -184,7 +190,9 @@ class ServerRSocket extends ServerAdapter {
|
||||
.then((client) => {
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Invalid JSON: ${payload.data}`,
|
||||
`[conn] Invalid JSON: ${payload.data}`,
|
||||
'clientMessage',
|
||||
'server',
|
||||
);
|
||||
@@ -263,7 +271,10 @@ class ServerRSocket extends ServerAdapter {
|
||||
this._onHandleUntrustedMessage(clientQuery, rawData)
|
||||
.then((_) => {})
|
||||
.catch((err) => {
|
||||
console.error('Unable to process CSR, failed with error.', err);
|
||||
console.error(
|
||||
'[conn] Unable to process CSR, failed with error.',
|
||||
err,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,11 +51,16 @@ class ServerWebSocket extends ServerWebSocketBase {
|
||||
const query = querystring.decode(message.url.split('?')[1]);
|
||||
const clientQuery = this._parseClientQuery(query);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[conn] Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`,
|
||||
);
|
||||
this.listener.onConnectionAttempt(clientQuery);
|
||||
|
||||
ws.on('message', async (message: any) => {
|
||||
@@ -78,10 +83,15 @@ class ServerWebSocket extends ServerWebSocketBase {
|
||||
const query = querystring.decode(message.url.split('?')[1]);
|
||||
const clientQuery = this._parseSecureClientQuery(query);
|
||||
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();
|
||||
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);
|
||||
|
||||
const pendingRequests: Map<
|
||||
@@ -116,7 +126,14 @@ class ServerWebSocket extends ServerWebSocketBase {
|
||||
clientQuery,
|
||||
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) => {
|
||||
let json: any | undefined;
|
||||
|
||||
@@ -72,7 +72,7 @@ abstract class ServerWebSocketBase extends ServerAdapter {
|
||||
handleRequest.apply(self, [ws, message]);
|
||||
});
|
||||
rawServer.on('error', (_ws: WebSocket, error: any) => {
|
||||
console.warn('Server found connection error: ' + error);
|
||||
console.warn('[conn] Server found connection error: ' + error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
|
||||
@@ -27,9 +27,15 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
|
||||
|
||||
verifyClient(): ws.VerifyClientCallbackSync {
|
||||
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),
|
||||
);
|
||||
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?
|
||||
};
|
||||
|
||||
console.log(
|
||||
`[conn] Local websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`,
|
||||
);
|
||||
this.listener.onConnectionAttempt(clientQuery);
|
||||
|
||||
const cleanup = () => {
|
||||
@@ -104,6 +113,10 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
|
||||
const extendedClientQuery = {...clientQuery, medium: 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;
|
||||
const client: Promise<Client> = this.listener.onConnectionCreated(
|
||||
extendedClientQuery,
|
||||
@@ -111,10 +124,16 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
|
||||
);
|
||||
client
|
||||
.then((client) => {
|
||||
console.log(
|
||||
`[conn] Client created: ${clientQuery.app} on ${clientQuery.device_id}.`,
|
||||
);
|
||||
resolvedClient = client;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to connect client over webSocket', e);
|
||||
console.error(
|
||||
'[conn] Failed to connect client over webSocket',
|
||||
e,
|
||||
);
|
||||
});
|
||||
|
||||
clients[app] = client;
|
||||
@@ -125,7 +144,7 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
|
||||
parsed = JSON.parse(m.toString());
|
||||
} catch (error) {
|
||||
// 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;
|
||||
}
|
||||
// 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. */
|
||||
ws.on('error', (error) => {
|
||||
console.warn('Server found connection error: ' + error);
|
||||
console.warn('[conn] Server found connection error: ' + error);
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@ export default class CertificateProvider {
|
||||
this.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.server = server;
|
||||
@@ -128,10 +132,10 @@ export default class CertificateProvider {
|
||||
certificate_zip: file,
|
||||
device_id: deviceID,
|
||||
}),
|
||||
'Timed out uploading Flipper export.',
|
||||
'Timed out uploading Flipper certificates to WWW.',
|
||||
),
|
||||
'uploadCertificates',
|
||||
).catch((e) => console.error(`Failed to upload certificates due to ${e}`));
|
||||
);
|
||||
};
|
||||
|
||||
async processCertificateSigningRequest(
|
||||
@@ -148,71 +152,60 @@ export default class CertificateProvider {
|
||||
const rootFolder = await promisify(tmp.dir)();
|
||||
const certFolder = rootFolder + '/FlipperCerts/';
|
||||
const certsZipPath = rootFolder + '/certs.zip';
|
||||
return this.certificateSetup
|
||||
.then((_) => this.getCACertificate())
|
||||
.then((caCert) =>
|
||||
this.deployOrStageFileForMobileApp(
|
||||
appDirectory,
|
||||
deviceCAcertFile,
|
||||
caCert,
|
||||
csr,
|
||||
os,
|
||||
medium,
|
||||
certFolder,
|
||||
),
|
||||
)
|
||||
.then((_) => this.generateClientCertificate(csr))
|
||||
.then((clientCert) =>
|
||||
this.deployOrStageFileForMobileApp(
|
||||
appDirectory,
|
||||
deviceClientCertFile,
|
||||
clientCert,
|
||||
csr,
|
||||
os,
|
||||
medium,
|
||||
certFolder,
|
||||
),
|
||||
)
|
||||
.then((_) => {
|
||||
return this.extractAppNameFromCSR(csr);
|
||||
})
|
||||
.then((appName) => {
|
||||
if (medium === 'FS_ACCESS') {
|
||||
return this.getTargetDeviceId(os, appName, appDirectory, csr);
|
||||
} else {
|
||||
return uuid();
|
||||
}
|
||||
})
|
||||
.then(async (deviceId) => {
|
||||
if (medium === 'WWW') {
|
||||
const zipPromise = new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(certsZipPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: {level: 9}, // Sets the compression level.
|
||||
});
|
||||
archive.directory(certFolder, false);
|
||||
output.on('close', function () {
|
||||
resolve(certsZipPath);
|
||||
});
|
||||
archive.on('warning', reject);
|
||||
archive.on('error', reject);
|
||||
archive.pipe(output);
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
await reportPlatformFailures(
|
||||
zipPromise,
|
||||
'www-certs-exchange-zipping-certs',
|
||||
);
|
||||
await reportPlatformFailures(
|
||||
this.uploadFiles(certsZipPath, deviceId),
|
||||
'www-certs-exchange-uploading-certs',
|
||||
);
|
||||
}
|
||||
return {
|
||||
deviceId,
|
||||
};
|
||||
await this.certificateSetup;
|
||||
const caCert = await this.getCACertificate();
|
||||
await this.deployOrStageFileForMobileApp(
|
||||
appDirectory,
|
||||
deviceCAcertFile,
|
||||
caCert,
|
||||
csr,
|
||||
os,
|
||||
medium,
|
||||
certFolder,
|
||||
);
|
||||
const clientCert = await this.generateClientCertificate(csr);
|
||||
await this.deployOrStageFileForMobileApp(
|
||||
appDirectory,
|
||||
deviceClientCertFile,
|
||||
clientCert,
|
||||
csr,
|
||||
os,
|
||||
medium,
|
||||
certFolder,
|
||||
);
|
||||
const appName = await this.extractAppNameFromCSR(csr);
|
||||
const deviceId =
|
||||
medium === 'FS_ACCESS'
|
||||
? await this.getTargetDeviceId(os, appName, appDirectory, csr)
|
||||
: uuid();
|
||||
if (medium === 'WWW') {
|
||||
const zipPromise = new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(certsZipPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: {level: 9}, // Sets the compression level.
|
||||
});
|
||||
archive.directory(certFolder, false);
|
||||
output.on('close', function () {
|
||||
resolve(certsZipPath);
|
||||
});
|
||||
archive.on('warning', reject);
|
||||
archive.on('error', reject);
|
||||
archive.pipe(output);
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
await reportPlatformFailures(
|
||||
zipPromise,
|
||||
'www-certs-exchange-zipping-certs',
|
||||
);
|
||||
await reportPlatformFailures(
|
||||
this.uploadFiles(certsZipPath, deviceId),
|
||||
'www-certs-exchange-uploading-certs',
|
||||
);
|
||||
}
|
||||
return {
|
||||
deviceId,
|
||||
};
|
||||
}
|
||||
|
||||
getTargetDeviceId(
|
||||
@@ -276,64 +269,56 @@ export default class CertificateProvider {
|
||||
medium: CertificateExchangeMedium,
|
||||
certFolder: string,
|
||||
): Promise<void> {
|
||||
const appNamePromise = this.extractAppNameFromCSR(csr);
|
||||
|
||||
if (medium === 'WWW') {
|
||||
const certPathExists = await fs.pathExists(certFolder);
|
||||
if (!certPathExists) {
|
||||
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(
|
||||
`Failed to write ${filename} to temporary folder. Error: ${e}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const appName = await this.extractAppNameFromCSR(csr);
|
||||
|
||||
if (os === 'Android') {
|
||||
const deviceIdPromise = appNamePromise.then((app) =>
|
||||
this.getTargetAndroidDeviceId(app, destination, csr),
|
||||
const deviceId = await this.getTargetAndroidDeviceId(
|
||||
appName,
|
||||
destination,
|
||||
csr,
|
||||
);
|
||||
return Promise.all([deviceIdPromise, appNamePromise, this.adb]).then(
|
||||
([deviceId, appName, adbClient]) =>
|
||||
androidUtil.push(
|
||||
adbClient,
|
||||
deviceId,
|
||||
appName,
|
||||
destination + filename,
|
||||
contents,
|
||||
),
|
||||
const adbClient = await this.adb;
|
||||
await androidUtil.push(
|
||||
adbClient,
|
||||
deviceId,
|
||||
appName,
|
||||
destination + filename,
|
||||
contents,
|
||||
);
|
||||
} else if (os === 'iOS') {
|
||||
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}`);
|
||||
}
|
||||
if (os === 'iOS' || os === 'windows' || os == 'MacOS') {
|
||||
return fs
|
||||
.writeFile(destination + filename, contents)
|
||||
.catch(async (err) => {
|
||||
if (os === 'iOS') {
|
||||
// Writing directly to FS failed. It's probably a physical device.
|
||||
const relativePathInsideApp =
|
||||
this.getRelativePathInAppContainer(destination);
|
||||
const appName = await appNamePromise;
|
||||
const udid = await this.getTargetiOSDeviceId(
|
||||
appName,
|
||||
destination,
|
||||
csr,
|
||||
);
|
||||
return await this.pushFileToiOSDevice(
|
||||
udid,
|
||||
appName,
|
||||
relativePathInsideApp,
|
||||
filename,
|
||||
contents,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid appDirectory recieved from ${os} device: ${destination}: ` +
|
||||
err.toString(),
|
||||
);
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(`Unsupported device os: ${os}`));
|
||||
}
|
||||
|
||||
private async pushFileToiOSDevice(
|
||||
@@ -346,7 +331,7 @@ export default class CertificateProvider {
|
||||
const dir = await tmpDir({unsafeCleanup: true});
|
||||
const filePath = path.resolve(dir, filename);
|
||||
await fs.writeFile(filePath, contents);
|
||||
return await iosUtil.push(
|
||||
await iosUtil.push(
|
||||
udid,
|
||||
filePath,
|
||||
bundleId,
|
||||
@@ -360,11 +345,11 @@ export default class CertificateProvider {
|
||||
deviceCsrFilePath: string,
|
||||
csr: string,
|
||||
): Promise<string> {
|
||||
const devices = await this.adb.then((client) => client.listDevices());
|
||||
if (devices.length === 0) {
|
||||
const devicesInAdb = await this.adb.then((client) => client.listDevices());
|
||||
if (devicesInAdb.length === 0) {
|
||||
throw new Error('No Android devices found');
|
||||
}
|
||||
const deviceMatchList = devices.map(async (device) => {
|
||||
const deviceMatchList = devicesInAdb.map(async (device) => {
|
||||
try {
|
||||
const result = await this.androidDeviceHasMatchingCSR(
|
||||
deviceCsrFilePath,
|
||||
@@ -381,33 +366,32 @@ export default class CertificateProvider {
|
||||
return {id: device.id, isMatch: false, foundCsr: null, error: e};
|
||||
}
|
||||
});
|
||||
return Promise.all(deviceMatchList).then((devices) => {
|
||||
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):
|
||||
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('More than one matching device found for CSR'),
|
||||
csr,
|
||||
);
|
||||
}
|
||||
return matchingIds[0];
|
||||
});
|
||||
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(
|
||||
@@ -418,7 +402,7 @@ export default class CertificateProvider {
|
||||
const matches = /\/Devices\/([^/]+)\//.exec(deviceCsrFilePath);
|
||||
if (matches && matches.length == 2) {
|
||||
// It's a simulator, the deviceId is in the filepath.
|
||||
return Promise.resolve(matches[1]);
|
||||
return matches[1];
|
||||
}
|
||||
const targets = await iosUtil.targets(
|
||||
this.config.idbPath,
|
||||
@@ -436,40 +420,38 @@ export default class CertificateProvider {
|
||||
);
|
||||
return {id: target.udid, isMatch};
|
||||
});
|
||||
return Promise.all(deviceMatchList).then((devices) => {
|
||||
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}`);
|
||||
}
|
||||
return matchingIds[0];
|
||||
});
|
||||
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 androidDeviceHasMatchingCSR(
|
||||
private async androidDeviceHasMatchingCSR(
|
||||
directory: string,
|
||||
deviceId: string,
|
||||
processName: string,
|
||||
csr: string,
|
||||
): Promise<{isMatch: boolean; foundCsr: string}> {
|
||||
return this.adb
|
||||
.then((adbClient) =>
|
||||
androidUtil.pull(
|
||||
adbClient,
|
||||
deviceId,
|
||||
processName,
|
||||
directory + csrFileName,
|
||||
),
|
||||
)
|
||||
.then((deviceCsr) => {
|
||||
// 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};
|
||||
});
|
||||
const adbClient = await this.adb;
|
||||
const deviceCsr = await androidUtil.pull(
|
||||
adbClient,
|
||||
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(
|
||||
@@ -553,59 +535,57 @@ export default class CertificateProvider {
|
||||
|
||||
private async checkCertIsValid(filename: string): Promise<void> {
|
||||
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
|
||||
// expiring in the future, not those 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
|
||||
// reliable, keeping both checks for safety.
|
||||
return openssl('x509', {
|
||||
checkend: minCertExpiryWindowSeconds,
|
||||
in: filename,
|
||||
})
|
||||
.then(() => undefined)
|
||||
.catch((e) => {
|
||||
console.warn(`Certificate will expire soon: ${filename}`, logTag);
|
||||
throw e;
|
||||
})
|
||||
.then((_) =>
|
||||
openssl('x509', {
|
||||
enddate: true,
|
||||
in: filename,
|
||||
noout: true,
|
||||
}),
|
||||
)
|
||||
.then((endDateOutput) => {
|
||||
const dateString = endDateOutput.trim().split('=')[1].trim();
|
||||
const expiryDate = Date.parse(dateString);
|
||||
if (isNaN(expiryDate)) {
|
||||
console.error(
|
||||
'Unable to parse certificate expiry date: ' + endDateOutput,
|
||||
);
|
||||
throw new Error(
|
||||
'Cannot parse certificate expiry date. Assuming it has expired.',
|
||||
);
|
||||
}
|
||||
if (expiryDate <= Date.now() + minCertExpiryWindowSeconds * 1000) {
|
||||
throw new Error('Certificate has expired or will expire soon.');
|
||||
}
|
||||
try {
|
||||
await openssl('x509', {
|
||||
checkend: minCertExpiryWindowSeconds,
|
||||
in: filename,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Checking if certificate expire soon: ${filename}`,
|
||||
logTag,
|
||||
e,
|
||||
);
|
||||
const endDateOutput = await openssl('x509', {
|
||||
enddate: true,
|
||||
in: filename,
|
||||
noout: true,
|
||||
});
|
||||
const dateString = endDateOutput.trim().split('=')[1].trim();
|
||||
const expiryDate = Date.parse(dateString);
|
||||
if (isNaN(expiryDate)) {
|
||||
console.error(
|
||||
'Unable to parse certificate expiry date: ' + endDateOutput,
|
||||
);
|
||||
throw new Error(
|
||||
'Cannot parse certificate expiry date. Assuming it has expired.',
|
||||
);
|
||||
}
|
||||
if (expiryDate <= Date.now() + minCertExpiryWindowSeconds * 1000) {
|
||||
throw new Error('Certificate has expired or will expire soon.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private verifyServerCertWasIssuedByCA() {
|
||||
private async verifyServerCertWasIssuedByCA() {
|
||||
const options: {
|
||||
[key: string]: any;
|
||||
} = {CAfile: caCert};
|
||||
options[serverCert] = false;
|
||||
return openssl('verify', options).then((output) => {
|
||||
const verified = output.match(/[^:]+: OK/);
|
||||
if (!verified) {
|
||||
// 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.
|
||||
throw new Error('Current server cert was not issued by current CA');
|
||||
}
|
||||
});
|
||||
const output = await openssl('verify', options);
|
||||
const verified = output.match(/[^:]+: OK/);
|
||||
if (!verified) {
|
||||
// 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.
|
||||
throw new Error('Current server cert was not issued by current CA');
|
||||
}
|
||||
}
|
||||
|
||||
private async generateCertificateAuthority(): Promise<void> {
|
||||
@@ -613,21 +593,18 @@ export default class CertificateProvider {
|
||||
await fs.mkdir(getFilePath(''));
|
||||
}
|
||||
console.log('Generating new CA', logTag);
|
||||
return openssl('genrsa', {out: caKey, '2048': false})
|
||||
.then((_) =>
|
||||
openssl('req', {
|
||||
new: true,
|
||||
x509: true,
|
||||
subj: caSubject,
|
||||
key: caKey,
|
||||
out: caCert,
|
||||
}),
|
||||
)
|
||||
.then((_) => undefined);
|
||||
await openssl('genrsa', {out: caKey, '2048': false});
|
||||
await openssl('req', {
|
||||
new: true,
|
||||
x509: true,
|
||||
subj: caSubject,
|
||||
key: caKey,
|
||||
out: caCert,
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureServerCertExists(): Promise<void> {
|
||||
const allExist = Promise.all([
|
||||
const allExist = await Promise.all([
|
||||
fs.pathExists(serverKey),
|
||||
fs.pathExists(serverCert),
|
||||
fs.pathExists(caCert),
|
||||
@@ -636,37 +613,34 @@ export default class CertificateProvider {
|
||||
return this.generateServerCertificate();
|
||||
}
|
||||
|
||||
return this.checkCertIsValid(serverCert)
|
||||
.then(() => this.verifyServerCertWasIssuedByCA())
|
||||
.catch(() => this.generateServerCertificate());
|
||||
try {
|
||||
await this.checkCertIsValid(serverCert);
|
||||
await this.verifyServerCertWasIssuedByCA();
|
||||
} catch (e) {
|
||||
console.warn('Not all certs are valid, generating new ones', e);
|
||||
await this.generateServerCertificate();
|
||||
}
|
||||
}
|
||||
|
||||
private generateServerCertificate(): Promise<void> {
|
||||
return this.ensureCertificateAuthorityExists()
|
||||
.then((_) => {
|
||||
console.warn('Creating new server cert', logTag);
|
||||
})
|
||||
.then((_) => openssl('genrsa', {out: serverKey, '2048': false}))
|
||||
.then((_) =>
|
||||
openssl('req', {
|
||||
new: true,
|
||||
key: serverKey,
|
||||
out: serverCsr,
|
||||
subj: serverSubject,
|
||||
}),
|
||||
)
|
||||
.then((_) =>
|
||||
openssl('x509', {
|
||||
req: true,
|
||||
in: serverCsr,
|
||||
CA: caCert,
|
||||
CAkey: caKey,
|
||||
CAcreateserial: true,
|
||||
CAserial: serverSrl,
|
||||
out: serverCert,
|
||||
}),
|
||||
)
|
||||
.then((_) => undefined);
|
||||
private async generateServerCertificate(): Promise<void> {
|
||||
await this.ensureCertificateAuthorityExists();
|
||||
console.warn('Creating new server cert', logTag);
|
||||
await openssl('genrsa', {out: serverKey, '2048': false});
|
||||
await openssl('req', {
|
||||
new: true,
|
||||
key: serverKey,
|
||||
out: serverCsr,
|
||||
subj: serverSubject,
|
||||
});
|
||||
await openssl('x509', {
|
||||
req: true,
|
||||
in: serverCsr,
|
||||
CA: caCert,
|
||||
CAkey: caKey,
|
||||
CAcreateserial: true,
|
||||
CAserial: serverSrl,
|
||||
out: serverCert,
|
||||
});
|
||||
}
|
||||
|
||||
private async writeToTempFile(content: string): Promise<string> {
|
||||
|
||||
@@ -30,7 +30,7 @@ export type CrashLog = {
|
||||
|
||||
export function devicePlugin(client: DevicePluginClient) {
|
||||
let notificationID = -1;
|
||||
let watcher: Promise<FSWatcher | undefined>;
|
||||
let watcher: Promise<FSWatcher | undefined> | undefined = undefined;
|
||||
|
||||
const crashes = createState<Crash[]>([], {persist: 'crashes'});
|
||||
const selectedCrash = createState<string | undefined>();
|
||||
@@ -71,7 +71,7 @@ export function devicePlugin(client: DevicePluginClient) {
|
||||
|
||||
client.onDestroy(() => {
|
||||
watcher
|
||||
.then((watcher) => watcher?.close())
|
||||
?.then((watcher) => watcher?.close())
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
'[crash_reporter] FSWatcher failed resoving on destroy:',
|
||||
|
||||
Reference in New Issue
Block a user