Android Web Socket (#2978)
Summary: Pull Request resolved: https://github.com/facebook/flipper/pull/2978 Flipper Android WebSocket provider and socket implementation Reviewed By: ldelgadoj Differential Revision: D31683510 fbshipit-source-id: d553a7fdee9451da742e9ea3e6e5b6a2c9417579
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8596dd951b
commit
a935ab8a6c
@@ -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)
|
||||
|
||||
@@ -67,6 +67,7 @@ android {
|
||||
implementation deps.jsr305
|
||||
implementation deps.supportAppCompat
|
||||
implementation deps.supportSqlite
|
||||
implementation deps.websocket
|
||||
|
||||
testImplementation deps.mockito
|
||||
testImplementation deps.robolectric
|
||||
|
||||
@@ -13,16 +13,23 @@
|
||||
#include <fb/fbjni.h>
|
||||
#endif
|
||||
|
||||
#include <folly/io/async/AsyncSocketException.h>
|
||||
#include <folly/io/async/EventBase.h>
|
||||
#include <folly/io/async/EventBaseManager.h>
|
||||
|
||||
#include <folly/json.h>
|
||||
|
||||
#include <Flipper/ConnectionContextStore.h>
|
||||
#include <Flipper/FlipperClient.h>
|
||||
#include <Flipper/FlipperConnection.h>
|
||||
#include <Flipper/FlipperConnectionManager.h>
|
||||
#include <Flipper/FlipperResponder.h>
|
||||
#include <Flipper/FlipperSocket.h>
|
||||
#include <Flipper/FlipperSocketProvider.h>
|
||||
#include <Flipper/FlipperState.h>
|
||||
#include <Flipper/FlipperStateUpdateListener.h>
|
||||
#include <Flipper/FlipperTransportTypes.h>
|
||||
#include <Flipper/FlipperURLSerializer.h>
|
||||
|
||||
using namespace facebook;
|
||||
using namespace facebook::flipper;
|
||||
@@ -100,6 +107,321 @@ class JFlipperArray : public jni::JavaClass<JFlipperArray> {
|
||||
}
|
||||
};
|
||||
|
||||
class JFlipperSocketEventHandler
|
||||
: public jni::JavaClass<JFlipperSocketEventHandler> {
|
||||
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<JFlipperObject> reportAuthenticationChallengeReceived() {
|
||||
auto object = _certificateProvider();
|
||||
return make_global(object);
|
||||
}
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
SocketEventHandler _eventHandler;
|
||||
SocketMessageHandler _messageHandler;
|
||||
using CustomProvider = std::function<jni::local_ref<JFlipperObject>()>;
|
||||
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<JFlipperSocket> {};
|
||||
|
||||
class JFlipperSocketImpl
|
||||
: public jni::JavaClass<JFlipperSocketImpl, JFlipperSocket> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor =
|
||||
"Lcom/facebook/flipper/android/FlipperSocketImpl;";
|
||||
|
||||
static jni::local_ref<JFlipperSocketImpl> create(const std::string& url) {
|
||||
return newInstance(url);
|
||||
}
|
||||
|
||||
void connect() {
|
||||
static const auto method = getClass()->getMethod<void()>("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<void()>("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<void(std::string)>("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<JFlipperSocketEventHandler> eventHandler) {
|
||||
static const auto method =
|
||||
getClass()->getMethod<void(jni::alias_ref<JFlipperSocketEventHandler>)>(
|
||||
"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<facebook::flipper::FlipperSocketBasePayload> payload)
|
||||
: endpoint_(std::move(endpoint)), payload_(std::move(payload)) {}
|
||||
JFlipperWebSocket(
|
||||
facebook::flipper::FlipperConnectionEndpoint endpoint,
|
||||
std::unique_ptr<facebook::flipper::FlipperSocketBasePayload> 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<bool> 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<facebook::flipper::FlipperSocketBasePayload> payload_;
|
||||
facebook::flipper::ConnectionContextStore* connectionContextStore_;
|
||||
|
||||
facebook::flipper::SocketEventHandler eventHandler_;
|
||||
facebook::flipper::SocketMessageHandler messageHandler_;
|
||||
|
||||
jni::global_ref<JFlipperSocketImpl> socket_;
|
||||
};
|
||||
|
||||
class JFlipperSocketProvider : public facebook::flipper::FlipperSocketProvider {
|
||||
public:
|
||||
JFlipperSocketProvider() {}
|
||||
virtual std::unique_ptr<facebook::flipper::FlipperSocket> create(
|
||||
facebook::flipper::FlipperConnectionEndpoint endpoint,
|
||||
std::unique_ptr<facebook::flipper::FlipperSocketBasePayload> payload,
|
||||
folly::EventBase* eventBase) override {
|
||||
return std::make_unique<JFlipperWebSocket>(
|
||||
std::move(endpoint), std::move(payload));
|
||||
;
|
||||
}
|
||||
virtual std::unique_ptr<facebook::flipper::FlipperSocket> create(
|
||||
FlipperConnectionEndpoint endpoint,
|
||||
std::unique_ptr<FlipperSocketBasePayload> payload,
|
||||
folly::EventBase* eventBase,
|
||||
ConnectionContextStore* connectionContextStore) override {
|
||||
return std::make_unique<JFlipperWebSocket>(
|
||||
std::move(endpoint), std::move(payload), connectionContextStore);
|
||||
}
|
||||
};
|
||||
|
||||
class JFlipperResponder : public jni::JavaClass<JFlipperResponder> {
|
||||
public:
|
||||
constexpr static auto kJavaDescriptor =
|
||||
@@ -603,6 +925,8 @@ class JFlipperClient : public jni::HybridClass<JFlipperClient> {
|
||||
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<JFlipperClient> {
|
||||
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<JFlipperSocketProvider>());
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -638,6 +967,7 @@ jint JNI_OnLoad(JavaVM* vm, void*) {
|
||||
JFlipperConnectionImpl::registerNatives();
|
||||
JFlipperResponderImpl::registerNatives();
|
||||
JEventBase::registerNatives();
|
||||
JFlipperSocketEventHandlerImpl::registerNatives();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ public final class AndroidFlipperClient {
|
||||
sConnectionThread.getEventBase(),
|
||||
FlipperProps.getInsecurePort(),
|
||||
FlipperProps.getSecurePort(),
|
||||
FlipperProps.getAltInsecurePort(),
|
||||
FlipperProps.getAltSecurePort(),
|
||||
getServerHost(app),
|
||||
"Android",
|
||||
getFriendlyDeviceName(),
|
||||
|
||||
@@ -39,6 +39,8 @@ class FlipperClientImpl implements FlipperClient {
|
||||
EventBase connectionWorker,
|
||||
int insecurePort,
|
||||
int securePort,
|
||||
int altInsecurePort,
|
||||
int altSecurePort,
|
||||
String host,
|
||||
String os,
|
||||
String device,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -22,6 +22,7 @@ class ConnectionContextStore {
|
||||
std::string getCertificateSigningRequest();
|
||||
std::shared_ptr<folly::SSLContext> getSSLContext();
|
||||
std::string getCertificateDirectoryPath();
|
||||
std::string getCACertificatePath();
|
||||
std::string getDeviceId();
|
||||
void storeConnectionConfig(folly::dynamic& config);
|
||||
bool resetState();
|
||||
|
||||
Reference in New Issue
Block a user