diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 1812146ed..8bd93ba8a 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -41,6 +41,7 @@ set(external_DIR ${PROJECT_SOURCE_DIR}/third-party/external) set(libfolly_DIR ${external_DIR}/folly/) set(glog_DIR ${external_DIR}/glog) set(BOOST_DIR ${external_DIR}/boost/boost_1_63_0/) +set(OPENSSL_DIR ${external_DIR}/OpenSSL/openssl-1.1.1k/) set(LIBEVENT_DIR ${external_DIR}/LibEvent/libevent-2.1.11-stable/) set(build_DIR ${CMAKE_SOURCE_DIR}/build) @@ -52,7 +53,6 @@ file(MAKE_DIRECTORY ${build_DIR}) find_package(fbjni REQUIRED CONFIG) add_subdirectory(${libflipper_DIR} ${libflipper_build_DIR}) - target_include_directories(${PACKAGE_NAME} PRIVATE ${libjnihack_DIR} ${libflipper_DIR} @@ -65,6 +65,7 @@ target_include_directories(${PACKAGE_NAME} PRIVATE ${LIBEVENT_DIR}/ ${LIBEVENT_DIR}/include/ ${LIBEVENT_DIR}/include/event2 + ${OPENSSL_DIR}/include ) target_link_libraries(${PACKAGE_NAME} fbjni::fbjni flippercpp) diff --git a/android/build.gradle b/android/build.gradle index 1a3f44971..362ff61de 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -67,6 +67,7 @@ android { implementation deps.jsr305 implementation deps.supportAppCompat implementation deps.supportSqlite + implementation deps.websocket testImplementation deps.mockito testImplementation deps.robolectric diff --git a/android/src/main/cpp/sonar.cpp b/android/src/main/cpp/sonar.cpp index 6b37e35b6..72d474899 100644 --- a/android/src/main/cpp/sonar.cpp +++ b/android/src/main/cpp/sonar.cpp @@ -13,16 +13,23 @@ #include #endif +#include #include #include + #include +#include #include #include #include #include +#include +#include #include #include +#include +#include using namespace facebook; using namespace facebook::flipper; @@ -100,6 +107,321 @@ class JFlipperArray : public jni::JavaClass { } }; +class JFlipperSocketEventHandler + : public jni::JavaClass { + public: + constexpr static auto kJavaDescriptor = + "Lcom/facebook/flipper/core/FlipperSocketEventHandler;"; +}; + +class JFlipperWebSocket; +class JFlipperSocketEventHandlerImpl : public jni::HybridClass< + JFlipperSocketEventHandlerImpl, + JFlipperSocketEventHandler> { + public: + constexpr static auto kJavaDescriptor = + "Lcom/facebook/flipper/android/FlipperSocketEventHandlerImpl;"; + + static void registerNatives() { + registerHybrid({ + makeNativeMethod( + "reportConnectionEvent", + JFlipperSocketEventHandlerImpl::reportConnectionEvent), + makeNativeMethod( + "reportMessageReceived", + JFlipperSocketEventHandlerImpl::reportMessageReceived), + makeNativeMethod( + "reportAuthenticationChallengeReceived", + JFlipperSocketEventHandlerImpl:: + reportAuthenticationChallengeReceived), + }); + } + + void reportConnectionEvent(int code) { + _eventHandler((SocketEvent)code); + } + + void reportMessageReceived(const std::string& message) { + _messageHandler(message); + } + + jni::global_ref reportAuthenticationChallengeReceived() { + auto object = _certificateProvider(); + return make_global(object); + } + + private: + friend HybridBase; + SocketEventHandler _eventHandler; + SocketMessageHandler _messageHandler; + using CustomProvider = std::function()>; + CustomProvider _certificateProvider; + + JFlipperSocketEventHandlerImpl( + SocketEventHandler eventHandler, + SocketMessageHandler messageHandler, + CustomProvider certificateProvider) + : _eventHandler(std::move(eventHandler)), + _messageHandler(std::move(messageHandler)), + _certificateProvider(std::move(certificateProvider)) {} +}; + +class JFlipperSocket : public jni::JavaClass {}; + +class JFlipperSocketImpl + : public jni::JavaClass { + public: + constexpr static auto kJavaDescriptor = + "Lcom/facebook/flipper/android/FlipperSocketImpl;"; + + static jni::local_ref create(const std::string& url) { + return newInstance(url); + } + + void connect() { + static const auto method = getClass()->getMethod("flipperConnect"); + try { + method(self()); + } catch (const std::exception& e) { + handleException(e); + } catch (const std::exception* e) { + if (e) { + handleException(*e); + } + } + } + + void disconnect() { + static const auto method = + getClass()->getMethod("flipperDisconnect"); + try { + method(self()); + } catch (const std::exception& e) { + handleException(e); + } catch (const std::exception* e) { + if (e) { + handleException(*e); + } + } + } + + void send(const std::string& message) { + static const auto method = + getClass()->getMethod("flipperSend"); + try { + method(self(), message); + } catch (const std::exception& e) { + handleException(e); + } catch (const std::exception* e) { + if (e) { + handleException(*e); + } + } + } + + void setEventHandler( + jni::alias_ref eventHandler) { + static const auto method = + getClass()->getMethod)>( + "flipperSetEventHandler"); + try { + method(self(), eventHandler); + } catch (const std::exception& e) { + handleException(e); + } catch (const std::exception* e) { + if (e) { + handleException(*e); + } + } + } +}; + +class JFlipperWebSocket : public facebook::flipper::FlipperSocket { + public: + JFlipperWebSocket( + facebook::flipper::FlipperConnectionEndpoint endpoint, + std::unique_ptr payload) + : endpoint_(std::move(endpoint)), payload_(std::move(payload)) {} + JFlipperWebSocket( + facebook::flipper::FlipperConnectionEndpoint endpoint, + std::unique_ptr payload, + facebook::flipper::ConnectionContextStore* connectionContextStore) + : endpoint_(std::move(endpoint)), + payload_(std::move(payload)), + connectionContextStore_(connectionContextStore) {} + + virtual ~JFlipperWebSocket() { + disconnect(); + } + + virtual void setEventHandler(SocketEventHandler eventHandler) override { + eventHandler_ = std::move(eventHandler); + } + virtual void setMessageHandler(SocketMessageHandler messageHandler) override { + messageHandler_ = std::move(messageHandler); + } + + virtual bool connect(FlipperConnectionManager* manager) override { + if (socket_ != nullptr) { + return true; + } + + std::string connectionURL = endpoint_.secure ? "wss://" : "ws://"; + connectionURL += endpoint_.host; + connectionURL += ":"; + connectionURL += std::to_string(endpoint_.port); + + auto serializer = facebook::flipper::URLSerializer{}; + payload_->serialize(serializer); + auto payload = serializer.serialize(); + + if (payload.size()) { + connectionURL += "?"; + connectionURL += payload; + } + + auto secure = endpoint_.secure; + + bool fullfilled = false; + std::promise promise; + auto connected = promise.get_future(); + + socket_ = make_global(JFlipperSocketImpl::create(connectionURL)); + socket_->setEventHandler(JFlipperSocketEventHandlerImpl::newObjectCxxArgs( + [&fullfilled, &promise, eventHandler = eventHandler_]( + SocketEvent event) { + /** + Only fulfill the promise the first time the event handler is used. + If the open event is received, then set the promise value to true. + For any other event, consider a failure and set to false. + */ + if (!fullfilled) { + fullfilled = true; + if (event == SocketEvent::OPEN) { + promise.set_value(true); + } else if (event == SocketEvent::SSL_ERROR) { + try { + promise.set_exception( + std::make_exception_ptr(folly::AsyncSocketException( + folly::AsyncSocketException::SSL_ERROR, + "SSL handshake failed"))); + } catch (...) { + // set_exception() may throw an exception + // In that case, just set the value to false. + promise.set_value(false); + } + } else { + promise.set_value(false); + } + } + eventHandler(event); + }, + [messageHandler = messageHandler_](const std::string& message) { + messageHandler(message); + }, + [secure, store = connectionContextStore_]() { + folly::dynamic object_ = folly::dynamic::object(); + if (secure) { + auto certificate = store->getCertificate(); + if (certificate.first.length() == 0) { + return JFlipperObject::create(nullptr); + } + object_["certificates_client_path"] = certificate.first; + object_["certificates_client_pass"] = certificate.second; + object_["certificates_ca_path"] = store->getCACertificatePath(); + } + return JFlipperObject::create(std::move(object_)); + })); + socket_->connect(); + + auto state = connected.wait_for(std::chrono::seconds(10)); + if (state == std::future_status::ready) { + return connected.get(); + } + disconnect(); + return false; + } + + virtual void disconnect() override { + if (socket_ == nullptr) { + return; + } + socket_->disconnect(); + socket_ = nullptr; + } + + virtual void send(const folly::dynamic& message, SocketSendHandler completion) + override { + if (socket_ == nullptr) { + return; + } + std::string json = folly::toJson(message); + send(json, std::move(completion)); + } + + virtual void send(const std::string& message, SocketSendHandler completion) + override { + if (socket_ == nullptr) { + return; + } + socket_->send(message); + completion(); + } + + virtual void sendExpectResponse( + const std::string& message, + SocketSendExpectResponseHandler completion) override { + if (socket_ == nullptr) { + return; + } + + socket_->setEventHandler(JFlipperSocketEventHandlerImpl::newObjectCxxArgs( + [eventHandler = eventHandler_](SocketEvent event) { + eventHandler(event); + }, + [completion, message](const std::string& msg) { + completion(msg, false); + }, + []() { + folly::dynamic object_ = folly::dynamic::object(); + return JFlipperObject::create(std::move(object_)); + })); + + socket_->send(message); + } + + private: + facebook::flipper::FlipperConnectionEndpoint endpoint_; + std::unique_ptr payload_; + facebook::flipper::ConnectionContextStore* connectionContextStore_; + + facebook::flipper::SocketEventHandler eventHandler_; + facebook::flipper::SocketMessageHandler messageHandler_; + + jni::global_ref socket_; +}; + +class JFlipperSocketProvider : public facebook::flipper::FlipperSocketProvider { + public: + JFlipperSocketProvider() {} + virtual std::unique_ptr create( + facebook::flipper::FlipperConnectionEndpoint endpoint, + std::unique_ptr payload, + folly::EventBase* eventBase) override { + return std::make_unique( + std::move(endpoint), std::move(payload)); + ; + } + virtual std::unique_ptr create( + FlipperConnectionEndpoint endpoint, + std::unique_ptr payload, + folly::EventBase* eventBase, + ConnectionContextStore* connectionContextStore) override { + return std::make_unique( + std::move(endpoint), std::move(payload), connectionContextStore); + } +}; + class JFlipperResponder : public jni::JavaClass { public: constexpr static auto kJavaDescriptor = @@ -603,6 +925,8 @@ class JFlipperClient : public jni::HybridClass { JEventBase* connectionWorker, int insecurePort, int securePort, + int altInsecurePort, + int altSecurePort, const std::string host, const std::string os, const std::string device, @@ -621,7 +945,12 @@ class JFlipperClient : public jni::HybridClass { callbackWorker->eventBase(), connectionWorker->eventBase(), insecurePort, - securePort}); + securePort, + altInsecurePort, + altSecurePort}); + // To switch to a WebSocket provider, uncomment the line below. + // facebook::flipper::FlipperSocketProvider::setDefaultProvider( + // std::make_unique()); } private: @@ -638,6 +967,7 @@ jint JNI_OnLoad(JavaVM* vm, void*) { JFlipperConnectionImpl::registerNatives(); JFlipperResponderImpl::registerNatives(); JEventBase::registerNatives(); + JFlipperSocketEventHandlerImpl::registerNatives(); }); } diff --git a/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java b/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java index a3e473c08..aa7ce1a9d 100644 --- a/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java +++ b/android/src/main/java/com/facebook/flipper/android/AndroidFlipperClient.java @@ -44,6 +44,8 @@ public final class AndroidFlipperClient { sConnectionThread.getEventBase(), FlipperProps.getInsecurePort(), FlipperProps.getSecurePort(), + FlipperProps.getAltInsecurePort(), + FlipperProps.getAltSecurePort(), getServerHost(app), "Android", getFriendlyDeviceName(), diff --git a/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java b/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java index 53d9a1d2b..20643c32f 100644 --- a/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java +++ b/android/src/main/java/com/facebook/flipper/android/FlipperClientImpl.java @@ -39,6 +39,8 @@ class FlipperClientImpl implements FlipperClient { EventBase connectionWorker, int insecurePort, int securePort, + int altInsecurePort, + int altSecurePort, String host, String os, String device, diff --git a/android/src/main/java/com/facebook/flipper/android/FlipperProps.java b/android/src/main/java/com/facebook/flipper/android/FlipperProps.java index 1b447b543..0ac59bd47 100644 --- a/android/src/main/java/com/facebook/flipper/android/FlipperProps.java +++ b/android/src/main/java/com/facebook/flipper/android/FlipperProps.java @@ -16,20 +16,33 @@ import java.nio.charset.Charset; class FlipperProps { private static final String FLIPPER_PORTS_PROP_NAME = "flipper.ports"; + private static final String FLIPPER_ALT_PORTS_PROP_NAME = "flipper.alt.ports"; private static final int DEFAULT_INSECURE_PORT = 8089; private static final int DEFAULT_SECURE_PORT = 8088; + private static final int DEFAULT_ALT_INSECURE_PORT = 9089; + private static final int DEFAULT_ALT_SECURE_PORT = 9088; private static final String TAG = "Flipper"; static int getInsecurePort() { - String propValue = getFlipperPortsPropValue(); + String propValue = getFlipperDefaultPortsPropValue(); return extractIntFromPropValue(propValue, 0, DEFAULT_INSECURE_PORT); } static int getSecurePort() { - String propValue = getFlipperPortsPropValue(); + String propValue = getFlipperDefaultPortsPropValue(); return extractIntFromPropValue(propValue, 1, DEFAULT_SECURE_PORT); } + static int getAltInsecurePort() { + String propValue = getFlipperDefaultAltPortsPropValue(); + return extractIntFromPropValue(propValue, 0, DEFAULT_ALT_INSECURE_PORT); + } + + static int getAltSecurePort() { + String propValue = getFlipperDefaultAltPortsPropValue(); + return extractIntFromPropValue(propValue, 1, DEFAULT_ALT_SECURE_PORT); + } + static int extractIntFromPropValue(String propValue, int index, int fallback) { if (propValue != null && !propValue.isEmpty()) { try { @@ -38,7 +51,7 @@ class FlipperProps { return Integer.parseInt(values[index]); } } catch (NumberFormatException e) { - Log.e(TAG, "Failed to parse flipper.ports value: " + propValue); + Log.e(TAG, "Failed to parse flipper ports value: " + propValue); } } return fallback; @@ -46,15 +59,30 @@ class FlipperProps { private static String flipperPortsPropValue = null; - private static synchronized String getFlipperPortsPropValue() { + private static synchronized String getFlipperDefaultPortsPropValue() { if (flipperPortsPropValue != null) { return flipperPortsPropValue; } + flipperPortsPropValue = getFlipperPortsPropValue(FLIPPER_PORTS_PROP_NAME); + return flipperPortsPropValue; + } + + private static String flipperAltPortsPropValue = null; + + private static synchronized String getFlipperDefaultAltPortsPropValue() { + if (flipperAltPortsPropValue != null) { + return flipperAltPortsPropValue; + } + flipperAltPortsPropValue = getFlipperPortsPropValue(FLIPPER_ALT_PORTS_PROP_NAME); + return flipperAltPortsPropValue; + } + + private static synchronized String getFlipperPortsPropValue(String propsName) { + String propValue = null; Process process = null; BufferedReader reader = null; try { - process = - Runtime.getRuntime().exec(new String[] {"/system/bin/getprop", FLIPPER_PORTS_PROP_NAME}); + process = Runtime.getRuntime().exec(new String[] {"/system/bin/getprop", propsName}); reader = new BufferedReader( new InputStreamReader(process.getInputStream(), Charset.forName("UTF-8"))); @@ -64,22 +92,22 @@ class FlipperProps { while ((line = reader.readLine()) != null) { lastLine = line; } - flipperPortsPropValue = lastLine; + propValue = lastLine; } catch (IOException e) { - Log.e(TAG, "Failed to query for flipper.ports prop", e); - flipperPortsPropValue = ""; + Log.e(TAG, "Failed to query for flipper ports prop", e); + propValue = ""; } finally { try { if (reader != null) { reader.close(); } } catch (IOException e) { - Log.e(TAG, "Failed to close BufferedReader when reading flipper.ports prop", e); + Log.e(TAG, "Failed to close BufferedReader when reading flipper ports prop", e); } if (process != null) { process.destroy(); } } - return flipperPortsPropValue; + return propValue; } } diff --git a/android/src/main/java/com/facebook/flipper/android/FlipperSocketEventHandlerImpl.java b/android/src/main/java/com/facebook/flipper/android/FlipperSocketEventHandlerImpl.java new file mode 100644 index 000000000..b54256838 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/android/FlipperSocketEventHandlerImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.android; + +import com.facebook.flipper.core.FlipperObject; +import com.facebook.flipper.core.FlipperSocketEventHandler; +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; + +@DoNotStrip +class FlipperSocketEventHandlerImpl implements FlipperSocketEventHandler { + + private final HybridData mHybridData; + + private FlipperSocketEventHandlerImpl(HybridData hd) { + mHybridData = hd; + } + + @Override + public void onConnectionEvent(SocketEvent event) { + reportConnectionEvent(event.ordinal()); + } + + @Override + public void onMessageReceived(String message) { + reportMessageReceived(message); + } + + @Override + public FlipperObject onAuthenticationChallengeReceived() { + return reportAuthenticationChallengeReceived(); + } + + private native void reportConnectionEvent(int code); + + private native void reportMessageReceived(String message); + + private native FlipperObject reportAuthenticationChallengeReceived(); +} diff --git a/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java b/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java new file mode 100644 index 000000000..d03c37f6d --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/android/FlipperSocketImpl.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.android; + +import android.util.Log; +import com.facebook.flipper.BuildConfig; +import com.facebook.flipper.core.FlipperObject; +import com.facebook.flipper.core.FlipperSocket; +import com.facebook.flipper.core.FlipperSocketEventHandler; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.soloader.SoLoader; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.handshake.ServerHandshake; + +@DoNotStrip +class FlipperSocketImpl extends WebSocketClient implements FlipperSocket { + + private static final int CERTIFICATE_TTL_DAYS = 30; + + static { + if (BuildConfig.IS_INTERNAL_BUILD || BuildConfig.LOAD_FLIPPER_EXPLICIT) { + SoLoader.loadLibrary("flipper"); + } + } + + FlipperSocketEventHandler mEventHandler; + + FlipperSocketImpl(String url) throws URISyntaxException { + super(new URI(url)); + } + + public void flipperSetEventHandler(FlipperSocketEventHandler eventHandler) { + this.mEventHandler = eventHandler; + } + + @Override + public void flipperConnect() { + if ((this.isOpen())) { + return; + } + try { + /** + * Authentication object, if present, will be used to create a valid SSL context to establish + * a secure socket connection. If absent, then a connection will be established to perform the + * certificate exchange. + */ + FlipperObject authenticationObject = this.mEventHandler.onAuthenticationChallengeReceived(); + if (authenticationObject.contains("certificates_client_path") + && authenticationObject.contains("certificates_client_pass")) { + + SSLContext sslContext = SSLContext.getInstance("TLS"); + KeyStore ks = KeyStore.getInstance("PKCS12"); + + String cert_client_path = authenticationObject.getString("certificates_client_path"); + String cert_client_pass = authenticationObject.getString("certificates_client_pass"); + String cert_ca_path = authenticationObject.getString("certificates_ca_path"); + + ks.load(new FileInputStream(cert_client_path), cert_client_pass.toCharArray()); + + KeyManagerFactory kmf = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, cert_client_pass.toCharArray()); + + sslContext.init( + kmf.getKeyManagers(), new TrustManager[] {new FlipperTrustManager(cert_ca_path)}, null); + + SSLSocketFactory factory = sslContext.getSocketFactory(); + + this.setSocketFactory(factory); + } + + this.connect(); + } catch (Exception e) { + Log.e("Flipper", "Failed to initialize the socket before connect. " + e.getMessage()); + this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.ERROR); + } + } + + @Override + protected void onSetSSLParameters(SSLParameters sslParameters) { + sslParameters.setNeedClientAuth(true); + } + + @Override + public void onOpen(ServerHandshake handshakedata) { + this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.OPEN); + } + + @Override + public void onMessage(String message) { + this.mEventHandler.onMessageReceived(message); + } + + @Override + public void onClose(int code, String reason, boolean remote) { + this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.CLOSE); + } + + /** + * If no socket factory is set, a javax.net.ssl.SSLHandshakeException will be thrown with message: + * No subjectAltNanmes on the certificate match. If set, but without a key manager and/or trust + * manager, the same error will be thrown. + * + * @param ex + */ + @Override + public void onError(Exception ex) { + // Check the exception for OpenSSL error and change the event type. + // Required for Flipper as the current implementation treats these errors differently. + if (ex instanceof javax.net.ssl.SSLHandshakeException) { + this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.SSL_ERROR); + } else { + this.mEventHandler.onConnectionEvent(FlipperSocketEventHandler.SocketEvent.ERROR); + } + } + + @Override + public void flipperDisconnect() { + /** + * Set an event handler that does nothing, not interested in getting more socket event messages. + */ + this.mEventHandler = + new FlipperSocketEventHandler() { + @Override + public void onConnectionEvent(SocketEvent event) {} + + @Override + public void onMessageReceived(String message) {} + + @Override + public FlipperObject onAuthenticationChallengeReceived() { + return null; + } + }; + super.close(); + } + + @Override + public void flipperSend(String message) { + this.send(message); + } + + public class FlipperTrustManager implements X509TrustManager { + Certificate mCA; + + public FlipperTrustManager(String cert_ca_path) throws Exception { + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + InputStream caInputStream = new BufferedInputStream(new FileInputStream(cert_ca_path)); + + try { + mCA = certificateFactory.generateCertificate(caInputStream); + } finally { + caInputStream.close(); + } + + if (mCA == null) { + /** Unable to find a valid CA. */ + throw new Exception("Unable to find a valid trust manager."); + } + } + + public void checkClientTrusted(X509Certificate[] chain, String algorithm) + throws CertificateException { + throw new CertificateException("No client certificate verification provided"); + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + try { + checkServerTrustedImpl(chain); + } catch (Exception e) { + if (!(e instanceof CertificateException)) { + e = new CertificateException(e); + } + throw (CertificateException) e; + } + } + + public void checkServerTrustedImpl(X509Certificate[] chain) throws CertificateException { + /** + * If we expect a 2 certs chain (CA + certificate), lets enforce that (allow only 2 certs in + * the chain) - this would prevent attacks where validators fail to traverse the whole chain + * and then you can have an invalid chain that still passes the CA validation (it has been a + * common vulnerability in the past) + */ + if (chain.length != 2) { + throw new CertificateException("Certificate chain is invalid. Invalid length"); + } + + final Date now = new Date(); + for (X509Certificate certificate : chain) { + certificate.checkValidity(now); + + /** + * Ensure the certificates are considered invalid after a certain period of time, enforced + * by the mobile client instead of based on cert/CA expiration under desktop control, they + * are re-used (desktop app generate them and then they are re-used for subsequent + * connections) the client has a TTL (currently set at 30 days). + */ + final Date notBefore = certificate.getNotBefore(); + if (notBefore.after(now)) { + throw new CertificateException("Unable to accept certificate in the chain"); + } + + long diff = now.getTime() - notBefore.getTime(); + long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS); + if (days < 0 || days > CERTIFICATE_TTL_DAYS) { + throw new CertificateException("Unable to accept certificate in the chain"); + } + } + + // Check issued by trusted issuer + final CertPathValidator certificatePathValidator; + try { + certificatePathValidator = + CertPathValidator.getInstance(CertPathValidator.getDefaultType()); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException(e); + } + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + CertPath certificatePath = certificateFactory.generateCertPath(Arrays.asList(chain)); + + TrustAnchor trustAnchor = new TrustAnchor((X509Certificate) mCA, null); + + final PKIXParameters pkixParameters; + try { + pkixParameters = new PKIXParameters(Collections.singleton(trustAnchor)); + } catch (InvalidAlgorithmParameterException e) { + throw new CertificateException(e); + } + + pkixParameters.setDate(now); + /** + * Note: considering this is a self-signed CA generated by the desktop app and passed back to + * the android client then revocation should not be a concern. + */ + pkixParameters.setRevocationEnabled(false); + try { + certificatePathValidator.validate(certificatePath, pkixParameters); + } catch (CertPathValidatorException | InvalidAlgorithmParameterException e) { + throw new CertificateException(e); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {(X509Certificate) mCA}; + } + } +} diff --git a/android/src/main/java/com/facebook/flipper/core/FlipperSocket.java b/android/src/main/java/com/facebook/flipper/core/FlipperSocket.java new file mode 100644 index 000000000..909552b31 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/core/FlipperSocket.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.core; + +public interface FlipperSocket { + + /** Connect to the endpoint. */ + void flipperConnect(); + + /** Disconnect from the endpoint. */ + void flipperDisconnect(); + + /** + * Call a remote method on the Flipper desktop application, passing an optional JSON array as a + * parameter. + */ + void flipperSend(String message); + + /** Sets a socket event handler. */ + void flipperSetEventHandler(FlipperSocketEventHandler eventHandler); +} diff --git a/android/src/main/java/com/facebook/flipper/core/FlipperSocketEventHandler.java b/android/src/main/java/com/facebook/flipper/core/FlipperSocketEventHandler.java new file mode 100644 index 000000000..a67262c20 --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/core/FlipperSocketEventHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.flipper.core; + +public interface FlipperSocketEventHandler { + + enum SocketEvent { + OPEN(0), + CLOSE(1), + ERROR(2), + SSL_ERROR(3); + + private final int mCode; + + private SocketEvent(int code) { + this.mCode = code; + } + + public int getCode() { + return this.mCode; + } + } + + void onConnectionEvent(SocketEvent event); + + void onMessageReceived(String message); + + FlipperObject onAuthenticationChallengeReceived(); +} diff --git a/build.gradle b/build.gradle index 3b6f11333..45179d4fd 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,7 @@ ext.deps = [ fbjni : 'com.facebook.fbjni:fbjni:0.2.2', screenshot : 'com.facebook.testing.screenshot:core:0.14.0', boltsTasks : 'com.parse.bolts:bolts-tasks:1.4.0', + websocket : 'org.java-websocket:Java-WebSocket:1.5.1', // Annotations jsr305 : 'com.google.code.findbugs:jsr305:3.0.2', inferAnnotations : 'com.facebook.infer.annotation:infer-annotation:0.18.0', diff --git a/xplat/Flipper/ConnectionContextStore.cpp b/xplat/Flipper/ConnectionContextStore.cpp index b8b2d2a70..514eef14f 100644 --- a/xplat/Flipper/ConnectionContextStore.cpp +++ b/xplat/Flipper/ConnectionContextStore.cpp @@ -123,6 +123,10 @@ std::string ConnectionContextStore::getCertificateDirectoryPath() { return absoluteFilePath(""); } +std::string ConnectionContextStore::getCACertificatePath() { + return absoluteFilePath(FLIPPER_CA_FILE_NAME); +} + bool ConnectionContextStore::resetState() { // Clear in-memory state csr = ""; diff --git a/xplat/Flipper/ConnectionContextStore.h b/xplat/Flipper/ConnectionContextStore.h index 6a7214108..a8a2b0eb1 100644 --- a/xplat/Flipper/ConnectionContextStore.h +++ b/xplat/Flipper/ConnectionContextStore.h @@ -22,6 +22,7 @@ class ConnectionContextStore { std::string getCertificateSigningRequest(); std::shared_ptr getSSLContext(); std::string getCertificateDirectoryPath(); + std::string getCACertificatePath(); std::string getDeviceId(); void storeConnectionConfig(folly::dynamic& config); bool resetState();