From edff177c3f82150bf83b1caea813c178831ea924 Mon Sep 17 00:00:00 2001 From: John Knox Date: Fri, 31 Aug 2018 07:58:01 -0700 Subject: [PATCH] Give serial to the device during cert exchange so it can provide it whenever it connects Summary: Android devices don't always know their own serial. But we do a certificate exchange using adb from the desktop, so we can provide it in the response. After this the client will provide it every time it connects, so we can do things like filter plugins by device id. For apps that have already done cert exchange, they'll continue to use 'unknown' as their id until they do it again with an up to date version of sonar. We can think about forcefully stopping that, but I haven't done it. Reviewed By: danielbuechele Differential Revision: D9481460 fbshipit-source-id: f8932699711ebbec4260fabe32f87e6cdff920f2 --- src/Client.js | 2 +- src/server.js | 8 ++++--- src/utils/CertificateProvider.js | 37 ++++++++++++++++++++++++++---- xplat/Sonar/SonarWebSocketImpl.cpp | 35 +++++++++++++++++++++++++++- xplat/Sonar/SonarWebSocketImpl.h | 1 + 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/Client.js b/src/Client.js index 722e3bdc7..cb8672af2 100644 --- a/src/Client.js +++ b/src/Client.js @@ -21,7 +21,7 @@ export type ClientQuery = {| app: string, os: string, device: string, - device_id: ?string, + device_id: string, |}; type RequestMetadata = {method: string, id: number, params: ?Object}; diff --git a/src/server.js b/src/server.js index 4fb96ad89..25d668908 100644 --- a/src/server.js +++ b/src/server.js @@ -181,9 +181,11 @@ export default class Server extends EventEmitter { subscriber.onSubscribe(); this.certificateProvider .processCertificateSigningRequest(csr, clientData.os, destination) - .then(_ => { + .then(result => { subscriber.onComplete({ - data: JSON.stringify({}), + data: JSON.stringify({ + deviceId: result.deviceId, + }), metadata: '', }); }) @@ -241,7 +243,7 @@ export default class Server extends EventEmitter { addConnection(conn: ReactiveSocket, query: ClientQuery): Client { invariant(query, 'expected query'); - const id = `${query.app}-${query.os}-${query.device}`; + const id = `${query.app}-${query.os}-${query.device}-${query.device_id}`; console.debug(`Device connected: ${id}`, 'connection'); const client = new Client(id, query, conn, this.logger); diff --git a/src/utils/CertificateProvider.js b/src/utils/CertificateProvider.js index e9d0f65e6..060cd0514 100644 --- a/src/utils/CertificateProvider.js +++ b/src/utils/CertificateProvider.js @@ -80,7 +80,7 @@ export default class CertificateProvider { csr: string, os: string, appDirectory: string, - ): Promise { + ): Promise<{|deviceId: string|}> { this.ensureOpenSSLIsAvailable(); return this.certificateSetup .then(_ => this.getCACertificate()) @@ -102,7 +102,36 @@ export default class CertificateProvider { csr, os, ), - ); + ) + .then(_ => this.extractAppNameFromCSR(csr)) + .then(appName => this.getTargetDeviceId(os, appName, appDirectory, csr)) + .then(deviceId => { + return { + deviceId, + }; + }); + } + + getTargetDeviceId( + os: string, + appName: string, + appDirectory: string, + csr: string, + ): Promise { + if (os === 'Android') { + return this.getTargetAndroidDeviceId(appName, appDirectory, csr); + } else if (os === 'iOS') { + const matches = /\/Devices\/([^/]+)\//.exec(appDirectory); + if (matches === null || matches.length < 2) { + return Promise.reject( + new Error( + `iOS simulator directory doesn't match expected format: ${appDirectory}`, + ), + ); + } + return Promise.resolve(matches[1]); + } + return Promise.resolve('unknown'); } ensureOpenSSLIsAvailable(): void { @@ -150,7 +179,7 @@ export default class CertificateProvider { if (os === 'Android') { const appNamePromise = this.extractAppNameFromCSR(csr); const deviceIdPromise = appNamePromise.then(app => - this.getTargetDeviceId(app, destination, csr), + this.getTargetAndroidDeviceId(app, destination, csr), ); return Promise.all([deviceIdPromise, appNamePromise]).then( ([deviceId, appName]) => @@ -179,7 +208,7 @@ export default class CertificateProvider { return Promise.reject(new RecurringError(`Unsupported device os: ${os}`)); } - getTargetDeviceId( + getTargetAndroidDeviceId( appName: string, deviceCsrFilePath: string, csr: string, diff --git a/xplat/Sonar/SonarWebSocketImpl.cpp b/xplat/Sonar/SonarWebSocketImpl.cpp index 46727ada0..75468c801 100644 --- a/xplat/Sonar/SonarWebSocketImpl.cpp +++ b/xplat/Sonar/SonarWebSocketImpl.cpp @@ -35,6 +35,7 @@ #define SONAR_CA_FILE_NAME "sonarCA.crt" #define CLIENT_CERT_FILE_NAME "device.crt" #define PRIVATE_KEY_FILE "privateKey.pem" +#define CONNECTION_CONFIG_FILE "connection_config.json" #define WRONG_THREAD_EXIT_MSG \ "ERROR: Aborting sonar initialization because it's not running in the sonar thread." @@ -47,6 +48,7 @@ namespace facebook { namespace sonar { bool fileExists(std::string fileName); +void writeStringToFile(std::string content, std::string fileName); class ConnectionEvents : public rsocket::RSocketConnectionEvents { private: @@ -176,9 +178,12 @@ void SonarWebSocketImpl::doCertificateExchange() { void SonarWebSocketImpl::connectSecurely() { rsocket::SetupParameters parameters; folly::SocketAddress address; + + auto deviceId = getDeviceId(); + parameters.payload = rsocket::Payload(folly::toJson(folly::dynamic::object( "os", deviceData_.os)("device", deviceData_.device)( - "device_id", deviceData_.deviceId)("app", deviceData_.app))); + "device_id", deviceId)("app", deviceData_.app))); address.setFromHostPort(deviceData_.host, securePort); std::shared_ptr sslContext = @@ -242,6 +247,27 @@ void SonarWebSocketImpl::sendMessage(const folly::dynamic& message) { }); } +std::string SonarWebSocketImpl::getDeviceId() { + /* On android we can't reliably get the serial of the current device + So rely on our locally written config, which is provided by the + desktop app. + For backwards compatibility, when this isn't present, fall back to the + unreliable source. */ + auto gettingDeviceId = sonarState_->start("Get deviceId"); + std::string config = loadStringFromFile(absoluteFilePath(CONNECTION_CONFIG_FILE)); + auto maybeDeviceId = folly::parseJson(config)["deviceId"]; + std::string deviceId; + if (maybeDeviceId.isString()) { + deviceId = maybeDeviceId.getString(); + } else { + deviceId = deviceData_.deviceId; + } + if (deviceId.compare("unknown")) { + gettingDeviceId->complete(); + } + return deviceId; +} + bool SonarWebSocketImpl::isCertificateExchangeNeeded() { if (failedConnectionAttempts_ >= 2) { @@ -281,6 +307,8 @@ void SonarWebSocketImpl::requestSignedCertFromSonar() { client_->getRequester() ->requestResponse(rsocket::Payload(folly::toJson(message))) ->subscribe([this, gettingCert](rsocket::Payload p) { + auto response = p.moveDataToString(); + writeStringToFile(response, absoluteFilePath(CONNECTION_CONFIG_FILE)); gettingCert->complete(); SONAR_LOG("Certificate exchange complete."); // Disconnect after message sending is complete. @@ -338,6 +366,11 @@ std::string SonarWebSocketImpl::loadStringFromFile(std::string fileName) { return s; } +void writeStringToFile(std::string content, std::string fileName) { + std::ofstream out(fileName); + out << content; +} + std::string SonarWebSocketImpl::absoluteFilePath(const char* filename) { return std::string(deviceData_.privateAppDirectory + "/sonar/" + filename); } diff --git a/xplat/Sonar/SonarWebSocketImpl.h b/xplat/Sonar/SonarWebSocketImpl.h index 6a76eaddb..5c6030c15 100644 --- a/xplat/Sonar/SonarWebSocketImpl.h +++ b/xplat/Sonar/SonarWebSocketImpl.h @@ -66,6 +66,7 @@ class SonarWebSocketImpl : public SonarWebSocket { bool ensureSonarDirExists(); bool isRunningInOwnThread(); void sendLegacyCertificateRequest(folly::dynamic message); + std::string getDeviceId(); }; } // namespace sonar