Initial commit 🎉

fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2
Co-authored-by: Sebastian McKenzie <sebmck@fb.com>
Co-authored-by: John Knox <jknox@fb.com>
Co-authored-by: Emil Sjölander <emilsj@fb.com>
Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
Daniel Büchele
2018-04-13 08:38:06 -07:00
committed by Daniel Buchele
commit fbbf8cf16b
659 changed files with 87130 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.xplat.sonar">
</manifest>

60
xplat/CMakeLists.txt Normal file
View File

@@ -0,0 +1,60 @@
cmake_minimum_required (VERSION 3.6.0)
project(sonarcpp CXX)
set(CMAKE_VERBOSE_MAKEFILE on)
set(PACKAGE_NAME sonarcpp)
set(third_party_ndk ../android/build/third-party-ndk)
set(libfolly_DIR ${third_party_ndk}/folly/)
set(easywsclient_DIR ../libs/)
set(glog_DIR ${third_party_ndk}/glog)
set(BOOST_DIR ${third_party_ndk}/boost/boost_1_63_0/)
set(DOUBLECONVERSION_DIR ${third_party_ndk}/double-conversion/double-conversion-3.0.0/)
list(APPEND dir_list ./)
list(APPEND dir_list ./Sonar)
include_directories(${dir_list})
add_compile_options(-DFOLLY_NO_CONFIG
-DFB_SONARKIT_ENABLED=1
-DFOLLY_HAVE_MEMRCHR
-DFOLLY_MOBILE=1
-DFOLLY_USE_LIBCPP=1
-DFOLLY_HAVE_LIBJEMALLOC=0
-DFOLLY_HAVE_PREADV=0
-frtti
-fexceptions
-std=c++14
-Wno-error
-Wno-unused-local-typedefs
-Wno-unused-variable
-Wno-sign-compare
-Wno-comment
-Wno-return-type
-Wno-tautological-constant-compare
)
file(GLOB SOURCES Sonar/*.cpp)
add_library(${PACKAGE_NAME} SHARED ${SOURCES})
set(build_DIR ${CMAKE_SOURCE_DIR}/build)
set(libfolly_build_DIR ${build_DIR}/libfolly/${ANDROID_ABI})
set(easywsclient_build_DIR ${build_DIR}/easywsclient/${ANDROID_ABI})
file(MAKE_DIRECTORY ${build_DIR})
add_subdirectory(${libfolly_DIR} ${libfolly_build_DIR})
add_subdirectory(${easywsclient_DIR}/easywsclient ${easywsclient_build_DIR})
target_include_directories(${PACKAGE_NAME} PRIVATE
${libfolly_DIR}
${BOOST_DIR}
${BOOST_DIR}/../
${glog_DIR}
${glog_DIR}/../
${glog_DIR}/glog-0.3.5/src/
${easywsclient_DIR}
)
target_link_libraries(${PACKAGE_NAME} folly easywsclient glog double-conversion log)

View File

@@ -0,0 +1,223 @@
/*
* Copyright (c) 2004-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#include "CertificateUtils.h"
#include <fcntl.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <sys/stat.h>
#include <cstring>
void free(
EVP_PKEY* pKey,
X509_REQ* x509_req,
BIGNUM* bne,
BIO* privateKey,
BIO* csrBio);
bool generateCertSigningRequest(
const char* appId,
const char* csrFile,
const char* privateKeyFile) {
int ret = 0;
BIGNUM* bne = NULL;
int nVersion = 1;
int bits = 2048;
// Using 65537 as exponent
unsigned long e = RSA_F4;
X509_NAME* x509_name = NULL;
const char* subjectCountry = "US";
const char* subjectProvince = "CA";
const char* subjectCity = "Menlo Park";
const char* subjectOrganization = "Sonar";
const char* subjectCommon = appId;
X509_REQ* x509_req = X509_REQ_new();
EVP_PKEY* pKey = EVP_PKEY_new();
RSA* rsa = RSA_new();
BIO* privateKey = NULL;
BIO* csrBio = NULL;
EVP_PKEY_assign_RSA(pKey, rsa);
// Generate rsa key
bne = BN_new();
BN_set_flags(bne, BN_FLG_CONSTTIME);
ret = BN_set_word(bne, e);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = RSA_generate_key_ex(rsa, bits, bne, NULL);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
{
// Write private key to a file
int privateKeyFd =
open(privateKeyFile, O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR);
if (privateKeyFd < 0) {
free(pKey, x509_req, bne, privateKey, csrBio);
return -1;
}
FILE* privateKeyFp = fdopen(privateKeyFd, "w");
if (privateKeyFp == NULL) {
free(pKey, x509_req, bne, privateKey, csrBio);
return -1;
}
privateKey = BIO_new_fp(privateKeyFp, BIO_CLOSE);
ret =
PEM_write_bio_RSAPrivateKey(privateKey, rsa, NULL, NULL, 0, NULL, NULL);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
}
rsa = NULL;
ret = BIO_flush(privateKey);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_REQ_set_version(x509_req, nVersion);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
x509_name = X509_REQ_get_subject_name(x509_req);
ret = X509_NAME_add_entry_by_txt(
x509_name,
"C",
MBSTRING_ASC,
(const unsigned char*)subjectCountry,
-1,
-1,
0);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_NAME_add_entry_by_txt(
x509_name,
"ST",
MBSTRING_ASC,
(const unsigned char*)subjectProvince,
-1,
-1,
0);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_NAME_add_entry_by_txt(
x509_name,
"L",
MBSTRING_ASC,
(const unsigned char*)subjectCity,
-1,
-1,
0);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_NAME_add_entry_by_txt(
x509_name,
"O",
MBSTRING_ASC,
(const unsigned char*)subjectOrganization,
-1,
-1,
0);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_NAME_add_entry_by_txt(
x509_name,
"CN",
MBSTRING_ASC,
(const unsigned char*)subjectCommon,
-1,
-1,
0);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_REQ_set_pubkey(x509_req, pKey);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
ret = X509_REQ_sign(
x509_req, pKey, EVP_sha256()); // returns x509_req->signature->length
if (ret <= 0) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
{
// Write CSR to a file
int csrFd = open(csrFile, O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR);
if (csrFd < 0) {
free(pKey, x509_req, bne, privateKey, csrBio);
return -1;
}
FILE* csrFp = fdopen(csrFd, "w");
if (csrFp == NULL) {
free(pKey, x509_req, bne, privateKey, csrBio);
return -1;
}
csrBio = BIO_new_fp(csrFp, BIO_CLOSE);
ret = PEM_write_bio_X509_REQ(csrBio, x509_req);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
}
ret = BIO_flush(csrBio);
if (ret != 1) {
free(pKey, x509_req, bne, privateKey, csrBio);
return ret;
}
return (ret == 1);
}
void free(
EVP_PKEY* pKey,
X509_REQ* x509_req,
BIGNUM* bne,
BIO* privateKey,
BIO* csrBio) {
BN_free(bne);
X509_REQ_free(x509_req);
EVP_PKEY_free(pKey);
BIO_free_all(privateKey);
BIO_free_all(csrBio);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2004-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#ifndef CertificateUtils_hpp
#define CertificateUtils_hpp
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <stdio.h>
bool generateCertSigningRequest(
const char* appId,
const char* csrFile,
const char* privateKeyFile);
#endif /* CertificateUtils_hpp */

204
xplat/Sonar/SonarClient.cpp Normal file
View File

@@ -0,0 +1,204 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#include "SonarClient.h"
#include "SonarConnectionImpl.h"
#include "SonarResponderImpl.h"
#include "SonarWebSocketImpl.h"
#ifdef __ANDROID__
#include <android/log.h>
#define SONAR_LOG(message) \
__android_log_print(ANDROID_LOG_INFO, "sonar", "sonar: %s", message)
#else
#define SONAR_LOG(message) printf("sonar: %s", message)
#endif
#if FB_SONARKIT_ENABLED
namespace facebook {
namespace sonar {
static SonarClient* kInstance;
using folly::dynamic;
void SonarClient::init(SonarInitConfig config) {
kInstance =
new SonarClient(std::make_unique<SonarWebSocketImpl>(std::move(config)));
}
SonarClient* SonarClient::instance() {
return kInstance;
}
void SonarClient::addPlugin(std::shared_ptr<SonarPlugin> plugin) {
SONAR_LOG(("SonarClient::addPlugin " + plugin->identifier()).c_str());
std::lock_guard<std::mutex> lock(mutex_);
performAndReportError([this, plugin]() {
if (plugins_.find(plugin->identifier()) != plugins_.end()) {
throw std::out_of_range(
"plugin " + plugin->identifier() + " already added.");
}
plugins_[plugin->identifier()] = plugin;
if (connected_) {
refreshPlugins();
}
});
}
void SonarClient::removePlugin(std::shared_ptr<SonarPlugin> plugin) {
SONAR_LOG(("SonarClient::removePlugin " + plugin->identifier()).c_str());
std::lock_guard<std::mutex> lock(mutex_);
performAndReportError([this, plugin]() {
if (plugins_.find(plugin->identifier()) == plugins_.end()) {
throw std::out_of_range("plugin " + plugin->identifier() + " not added.");
}
disconnect(plugin);
plugins_.erase(plugin->identifier());
if (connected_) {
refreshPlugins();
}
});
}
std::shared_ptr<SonarPlugin> SonarClient::getPlugin(
const std::string& identifier) {
std::lock_guard<std::mutex> lock(mutex_);
if (plugins_.find(identifier) == plugins_.end()) {
return nullptr;
}
return plugins_.at(identifier);
}
bool SonarClient::hasPlugin(const std::string& identifier) {
std::lock_guard<std::mutex> lock(mutex_);
return plugins_.find(identifier) != plugins_.end();
}
void SonarClient::disconnect(std::shared_ptr<SonarPlugin> plugin) {
const auto& conn = connections_.find(plugin->identifier());
if (conn != connections_.end()) {
connections_.erase(plugin->identifier());
plugin->didDisconnect();
}
}
void SonarClient::refreshPlugins() {
dynamic message = dynamic::object("method", "refreshPlugins");
socket_->sendMessage(message);
}
void SonarClient::onConnected() {
SONAR_LOG("SonarClient::onConnected");
std::lock_guard<std::mutex> lock(mutex_);
connected_ = true;
}
void SonarClient::onDisconnected() {
SONAR_LOG("SonarClient::onDisconnected");
std::lock_guard<std::mutex> lock(mutex_);
connected_ = false;
performAndReportError([this]() {
for (const auto& iter : plugins_) {
disconnect(iter.second);
}
});
}
void SonarClient::onMessageReceived(const dynamic& message) {
std::lock_guard<std::mutex> lock(mutex_);
performAndReportError([this, &message]() {
const auto& method = message["method"];
const auto& params = message.getDefault("params");
std::unique_ptr<SonarResponderImpl> responder;
if (message.find("id") != message.items().end()) {
responder.reset(
new SonarResponderImpl(socket_.get(), message["id"].getInt()));
}
if (method == "getPlugins") {
dynamic identifiers = dynamic::array();
for (const auto& elem : plugins_) {
identifiers.push_back(elem.first);
}
dynamic response = dynamic::object("plugins", identifiers);
responder->success(response);
return;
}
if (method == "init") {
const auto identifier = params["plugin"].getString();
if (plugins_.find(identifier) == plugins_.end()) {
throw std::out_of_range(
"plugin " + identifier + " not found for method " +
method.getString());
}
const auto plugin = plugins_.at(identifier);
auto& conn = connections_[plugin->identifier()];
conn = std::make_shared<SonarConnectionImpl>(
socket_.get(), plugin->identifier());
plugin->didConnect(conn);
return;
}
if (method == "deinit") {
const auto identifier = params["plugin"].getString();
if (plugins_.find(identifier) == plugins_.end()) {
throw std::out_of_range(
"plugin " + identifier + " not found for method " +
method.getString());
}
const auto plugin = plugins_.at(identifier);
disconnect(plugin);
return;
}
if (method == "execute") {
const auto identifier = params["api"].getString();
if (connections_.find(identifier) == connections_.end()) {
throw std::out_of_range(
"connection " + identifier + " not found for method " +
method.getString());
}
const auto& conn = connections_.at(params["api"].getString());
conn->call(
params["method"].getString(),
params.getDefault("params"),
std::move(responder));
return;
}
dynamic response =
dynamic::object("message", "Received unknown method: " + method);
responder->error(response);
});
}
void SonarClient::performAndReportError(const std::function<void()>& func) {
try {
func();
} catch (std::exception& e) {
if (connected_) {
dynamic message = dynamic::object(
"error",
dynamic::object("message", e.what())("stacktrace", "<none>"));
socket_->sendMessage(message);
}
}
}
} // namespace sonar
} // namespace facebook
#endif

87
xplat/Sonar/SonarClient.h Normal file
View File

@@ -0,0 +1,87 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarConnectionImpl.h>
#include <Sonar/SonarInitConfig.h>
#include <Sonar/SonarPlugin.h>
#include <Sonar/SonarWebSocket.h>
#include <map>
#include <mutex>
namespace facebook {
namespace sonar {
class SonarClient : public SonarWebSocket::Callbacks {
public:
/**
Call before accessing instance with SonarClient::instance(). This will set up
all the state needed to establish a Sonar connection.
*/
static void init(SonarInitConfig config);
/**
Standard accessor for the shared SonarClient instance. This returns a
singleton instance to a shared SonarClient. First call to this function will
create the shared SonarClient. Must call SonarClient::initDeviceData() before
first call to SonarClient::instance().
*/
static SonarClient* instance();
/**
Only public for testing
*/
SonarClient(std::unique_ptr<SonarWebSocket> socket)
: socket_(std::move(socket)) {
socket_->setCallbacks(this);
}
void start() {
socket_->start();
}
void stop() {
socket_->stop();
}
void onConnected() override;
void onDisconnected() override;
void onMessageReceived(const folly::dynamic& message) override;
void addPlugin(std::shared_ptr<SonarPlugin> plugin);
void removePlugin(std::shared_ptr<SonarPlugin> plugin);
void refreshPlugins();
std::shared_ptr<SonarPlugin> getPlugin(const std::string& identifier);
template <typename P>
std::shared_ptr<P> getPlugin(const std::string& identifier) {
return std::static_pointer_cast<P>(getPlugin(identifier));
}
bool hasPlugin(const std::string& identifier);
private:
static SonarClient* instance_;
bool connected_ = false;
std::unique_ptr<SonarWebSocket> socket_;
std::map<std::string, std::shared_ptr<SonarPlugin>> plugins_;
std::map<std::string, std::shared_ptr<SonarConnectionImpl>> connections_;
std::mutex mutex_;
void performAndReportError(const std::function<void()>& func);
void disconnect(std::shared_ptr<SonarPlugin> plugin);
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarResponder.h>
#include <folly/json.h>
#include <functional>
#include <string>
namespace facebook {
namespace sonar {
/**
Represents a connection between the Desktop and mobile plugins
with corresponding identifiers.
*/
class SonarConnection {
public:
using SonarReceiver = std::function<
void(const folly::dynamic&, std::unique_ptr<SonarResponder>)>;
virtual ~SonarConnection() {}
/**
Invoke a method on the Sonar desktop plugin with with a matching identifier.
*/
virtual void send(
const std::string& method,
const folly::dynamic& params) = 0;
/**
Report an error to the Sonar desktop app
*/
virtual void error(
const std::string& message,
const std::string& stacktrace) = 0;
/**
Register a receiver to be notified of incoming calls of the given
method from the Sonar desktop plugin with a matching identifier.
*/
virtual void receive(
const std::string& method,
const SonarReceiver& receiver) = 0;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarConnection.h>
#include <Sonar/SonarWebSocket.h>
#include <map>
#include <string>
namespace facebook {
namespace sonar {
class SonarConnectionImpl : public SonarConnection {
public:
SonarConnectionImpl(SonarWebSocket* socket, const std::string& name)
: socket_(socket), name_(name) {}
void call(
const std::string& method,
const folly::dynamic& params,
std::unique_ptr<SonarResponder> responder) {
if (receivers_.find(method) == receivers_.end()) {
throw std::out_of_range("receiver " + method + " not found.");
}
receivers_.at(method)(params, std::move(responder));
}
void send(const std::string& method, const folly::dynamic& params) override {
folly::dynamic message = folly::dynamic::object("method", "execute")(
"params",
folly::dynamic::object("api", name_)("method", method)(
"params", params));
socket_->sendMessage(message);
}
void error(const std::string& message, const std::string& stacktrace)
override {
socket_->sendMessage(folly::dynamic::object(
"error",
folly::dynamic::object("message", message)("stacktrace", stacktrace)));
}
void receive(const std::string& method, const SonarReceiver& receiver)
override {
receivers_[method] = receiver;
}
private:
SonarWebSocket* socket_;
std::string name_;
std::map<std::string, SonarReceiver> receivers_;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <folly/io/async/EventBase.h>
#include <map>
namespace facebook {
namespace sonar {
struct DeviceData {
std::string host;
std::string os;
std::string device;
std::string deviceId;
std::string app;
std::string appId;
std::string privateAppDirectory;
};
struct SonarInitConfig {
/**
Map of client specific configuration data such as app name, device name, etc.
*/
DeviceData deviceData;
/**
EventBase on which client callbacks should be called.
*/
folly::EventBase* worker;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,33 @@
Pod::Spec.new do |spec|
spec.name = 'Sonar'
spec.version = '1.0.0'
spec.license = { :type => 'MIT' }
spec.homepage = 'https://github.com/facebook/sonar'
spec.summary = 'SonarKit core cpp code with network implementation'
spec.authors = 'Facebook'
# spec.prepare_command = 'mv src double-conversion'
spec.source = { :git => 'https://github.com/facebook/Sonar.git',
:branch => 'master' }
spec.module_name = 'Sonar'
spec.public_header_files = 'xplat/Sonar/*.h'
spec.source_files = 'xplat/Sonar/*.{h,cpp,m,mm}'
spec.libraries = "stdc++"
spec.dependency 'Folly'
spec.dependency 'EasyWSClient'
# spec.dependency 'boost-for-react-native'
#
# spec.dependency 'DoubleConversion'
# spec.dependency 'Folly'
# spec.dependency 'glog'
spec.compiler_flags = '-DFB_SONARKIT_ENABLED=1 -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_HAVE_LIBGFLAGS=0 -DFOLLY_HAVE_LIBJEMALLOC=0 -DFOLLY_HAVE_PREADV=0 -DFOLLY_HAVE_PWRITEV=0 -DFOLLY_HAVE_TFO=0 -DFOLLY_USE_SYMBOLIZER=0 -Wall
-std=c++14
-Wno-global-constructors'
# spec.header_mappings_dir = 'folly'
# spec.header_dir = 'folly'
# spec.preserve_paths = 'xplat/**/*'
# Pinning to the same version as React.podspec.
spec.platforms = { :ios => "8.0", :tvos => "9.2" }
spec.pod_target_xcconfig = { "USE_HEADERMAP" => "NO",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++14",
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/EasyWSClient\" \"$(PODS_ROOT)/DoubleConversion\"" }
end

42
xplat/Sonar/SonarPlugin.h Normal file
View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarConnection.h>
#include <string>
namespace facebook {
namespace sonar {
class SonarPlugin {
public:
virtual ~SonarPlugin() {}
/**
The plugin's identifier. This should map to a javascript plugin
with the same identifier to ensure messages are sent correctly.
*/
virtual std::string identifier() const = 0;
/**
Called when a connection has been established between this plugin
and the corresponding plugin on the Sonar desktop app. The provided
connection can be used to register method receivers as well as send
messages back to the desktop app.
*/
virtual void didConnect(std::shared_ptr<SonarConnection> conn) = 0;
/**
Called when a plugin has been disconnected and the SonarConnection
provided in didConnect is no longer valid to use.
*/
virtual void didDisconnect() = 0;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <folly/json.h>
namespace facebook {
namespace sonar {
/**
* SonarResponder is used to asynchronously respond to messages
* received from the Sonar desktop app.
*/
class SonarResponder {
public:
virtual ~SonarResponder(){};
/**
* Deliver a successful response to the Sonar desktop app.
*/
virtual void success(const folly::dynamic& response) const = 0;
/**
* Inform the Sonar desktop app of an error in handling the request.
*/
virtual void error(const folly::dynamic& response) const = 0;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarResponder.h>
#include <Sonar/SonarWebSocket.h>
#include <folly/json.h>
namespace facebook {
namespace sonar {
class SonarResponderImpl : public SonarResponder {
public:
SonarResponderImpl(SonarWebSocket* socket, int64_t responseID)
: socket_(socket), responseID_(responseID) {}
void success(const folly::dynamic& response) const override {
const folly::dynamic message =
folly::dynamic::object("id", responseID_)("success", response);
socket_->sendMessage(message);
}
void error(const folly::dynamic& response) const override {
const folly::dynamic message =
folly::dynamic::object("id", responseID_)("error", response);
socket_->sendMessage(message);
}
private:
SonarWebSocket* socket_;
int64_t responseID_;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <folly/json.h>
namespace facebook {
namespace sonar {
class SonarWebSocket {
public:
class Callbacks;
public:
virtual ~SonarWebSocket(){};
/**
Establishes a connection to the ws server.
*/
virtual void start() = 0;
/**
Closes an open connection to the ws server.
*/
virtual void stop() = 0;
/**
True if there's an open connection.
This method may block if the connection is busy.
*/
virtual bool isOpen() const = 0;
/**
Send message to the ws server.
*/
virtual void sendMessage(const folly::dynamic& message) = 0;
/**
Handler for connection and message receipt from the ws server.
The callbacks should be set before a connection is established.
*/
virtual void setCallbacks(Callbacks* callbacks) = 0;
};
class SonarWebSocket::Callbacks {
public:
virtual ~Callbacks(){};
virtual void onConnected() = 0;
virtual void onDisconnected() = 0;
virtual void onMessageReceived(const folly::dynamic& message) = 0;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,301 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#include "SonarWebSocketImpl.h"
#include <folly/String.h>
#include <folly/futures/Future.h>
#include <folly/io/async/SSLContext.h>
#include <folly/json.h>
#include <rsocket/Payload.h>
#include <rsocket/RSocket.h>
#include <rsocket/transports/tcp/TcpConnectionFactory.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fstream>
#include <iostream>
#include <thread>
#include "CertificateUtils.h"
#ifdef __ANDROID__
#include <android/log.h>
#define SONAR_LOG(message) \
__android_log_print(ANDROID_LOG_INFO, "sonar", "sonar: %s", message)
#else
#define SONAR_LOG(message) printf("sonar: %s", message)
#endif
#define CSR_FILE_NAME "app.csr"
#define SONAR_CA_FILE_NAME "sonarCA.crt"
#define CLIENT_CERT_FILE_NAME "device.crt"
#define PRIVATE_KEY_FILE "privateKey.pem"
static constexpr int reconnectIntervalSeconds = 2;
static constexpr int connectionKeepaliveSeconds = 2;
static constexpr int securePort = 8088;
static constexpr int insecurePort = 8089;
namespace facebook {
namespace sonar {
class ConnectionEvents : public rsocket::RSocketConnectionEvents {
private:
SonarWebSocketImpl* websocket_;
public:
ConnectionEvents(SonarWebSocketImpl* websocket) : websocket_(websocket) {}
void onConnected() {
websocket_->isOpen_ = true;
if (websocket_->connectionIsTrusted_) {
websocket_->callbacks_->onConnected();
}
}
void onDisconnected(const folly::exception_wrapper&) {
if (!websocket_->isOpen_)
return;
websocket_->isOpen_ = false;
websocket_->connectionIsTrusted_ = false;
if (websocket_->connectionIsTrusted_) {
websocket_->callbacks_->onDisconnected();
}
websocket_->reconnect();
}
void onClosed(const folly::exception_wrapper& e) {
onDisconnected(e);
}
};
class Responder : public rsocket::RSocketResponder {
private:
SonarWebSocketImpl* websocket_;
public:
Responder(SonarWebSocketImpl* websocket) : websocket_(websocket) {}
void handleFireAndForget(
rsocket::Payload request,
rsocket::StreamId streamId) {
const auto payload = request.moveDataToString();
websocket_->callbacks_->onMessageReceived(folly::parseJson(payload));
}
};
SonarWebSocketImpl::SonarWebSocketImpl(SonarInitConfig config)
: deviceData_(config.deviceData), worker_(config.worker) {}
SonarWebSocketImpl::~SonarWebSocketImpl() {
stop();
}
void SonarWebSocketImpl::start() {
folly::makeFuture()
.via(worker_->getEventBase())
.delayed(std::chrono::milliseconds(0))
.then([this]() { startSync(); });
}
void SonarWebSocketImpl::startSync() {
if (isOpen()) {
SONAR_LOG("Already connected");
return;
}
try {
if (isCertificateExchangeNeeded()) {
doCertificateExchange();
return;
}
connectSecurely();
} catch (const std::exception& e) {
std::string errors = folly::SSLContext::getErrors();
SONAR_LOG("Error connecting to sonar");
SONAR_LOG(e.what());
SONAR_LOG(errors.c_str());
failedConnectionAttempts_++;
reconnect();
}
}
void SonarWebSocketImpl::doCertificateExchange() {
SONAR_LOG("Starting certificate exchange");
rsocket::SetupParameters parameters;
folly::SocketAddress address;
parameters.payload = rsocket::Payload(
folly::toJson(folly::dynamic::object("os", deviceData_.os)));
address.setFromHostPort(deviceData_.host, insecurePort);
connectionIsTrusted_ = false;
client_ =
rsocket::RSocket::createConnectedClient(
std::make_unique<rsocket::TcpConnectionFactory>(
*worker_->getEventBase(), std::move(address)),
std::move(parameters),
nullptr,
std::chrono::seconds(connectionKeepaliveSeconds), // keepaliveInterval
nullptr, // stats
std::make_shared<ConnectionEvents>(this))
.get();
ensureSonarDirExists();
requestSignedCertFromSonar();
}
void SonarWebSocketImpl::connectSecurely() {
rsocket::SetupParameters parameters;
folly::SocketAddress address;
parameters.payload = rsocket::Payload(folly::toJson(folly::dynamic::object(
"os", deviceData_.os)("device", deviceData_.device)(
"device_id", deviceData_.deviceId)("app", deviceData_.app)));
address.setFromHostPort(deviceData_.host, securePort);
std::shared_ptr<folly::SSLContext> sslContext =
std::make_shared<folly::SSLContext>();
sslContext->loadTrustedCertificates(
absoluteFilePath(SONAR_CA_FILE_NAME).c_str());
sslContext->setVerificationOption(
folly::SSLContext::SSLVerifyPeerEnum::VERIFY);
sslContext->loadCertKeyPairFromFiles(
absoluteFilePath(CLIENT_CERT_FILE_NAME).c_str(),
absoluteFilePath(PRIVATE_KEY_FILE).c_str());
sslContext->authenticate(true, false);
connectionIsTrusted_ = true;
client_ =
rsocket::RSocket::createConnectedClient(
std::make_unique<rsocket::TcpConnectionFactory>(
*worker_->getEventBase(),
std::move(address),
std::move(sslContext)),
std::move(parameters),
std::make_shared<Responder>(this),
std::chrono::seconds(connectionKeepaliveSeconds), // keepaliveInterval
nullptr, // stats
std::make_shared<ConnectionEvents>(this))
.get();
failedConnectionAttempts_ = 0;
}
void SonarWebSocketImpl::reconnect() {
folly::makeFuture()
.via(worker_->getEventBase())
.delayed(std::chrono::seconds(reconnectIntervalSeconds))
.then([this]() { startSync(); });
}
void SonarWebSocketImpl::stop() {
client_->disconnect();
client_ = nullptr;
}
bool SonarWebSocketImpl::isOpen() const {
return isOpen_ && connectionIsTrusted_;
}
void SonarWebSocketImpl::setCallbacks(Callbacks* callbacks) {
callbacks_ = callbacks;
}
void SonarWebSocketImpl::sendMessage(const folly::dynamic& message) {
worker_->add([this, message]() {
if (client_) {
client_->getRequester()
->fireAndForget(rsocket::Payload(folly::toJson(message)))
->subscribe([]() {});
}
});
}
bool SonarWebSocketImpl::isCertificateExchangeNeeded() {
if (failedConnectionAttempts_ >= 2) {
auto format =
"Requesting fresh certificate exchange after %d failed connection attempts";
char buff[strlen(format) + 1];
sprintf(buff, format, failedConnectionAttempts_);
SONAR_LOG(buff);
return true;
}
std::string caCert = loadStringFromFile(absoluteFilePath(SONAR_CA_FILE_NAME));
std::string clientCert =
loadStringFromFile(absoluteFilePath(CLIENT_CERT_FILE_NAME));
std::string privateKey =
loadStringFromFile(absoluteFilePath(PRIVATE_KEY_FILE));
if (caCert == "" || clientCert == "" || privateKey == "") {
return true;
}
return false;
}
void SonarWebSocketImpl::requestSignedCertFromSonar() {
SONAR_LOG("Requesting new client certificate from Sonar");
std::string csr = loadStringFromFile(absoluteFilePath(CSR_FILE_NAME));
if (csr == "") {
generateCertSigningRequest(
deviceData_.appId.c_str(),
absoluteFilePath(CSR_FILE_NAME).c_str(),
absoluteFilePath(PRIVATE_KEY_FILE).c_str());
csr = loadStringFromFile(absoluteFilePath(CSR_FILE_NAME));
}
// Send CSR to Sonar desktop
folly::dynamic message = folly::dynamic::object("method", "signCertificate")(
"csr", csr.c_str())("destination", absoluteFilePath("").c_str());
worker_->add([this, message]() {
client_->getRequester()
->fireAndForget(rsocket::Payload(folly::toJson(message)))
->subscribe([this]() {
// Disconnect after message sending is complete.
// This will trigger a reconnect which should use the secure channel.
client_ = nullptr;
});
});
failedConnectionAttempts_ = 0;
}
std::string SonarWebSocketImpl::loadStringFromFile(std::string fileName) {
std::stringstream buffer;
std::ifstream stream;
std::string line;
stream.open(fileName.c_str());
if (!stream) {
SONAR_LOG(
std::string("ERROR: Unable to open ifstream: " + fileName).c_str());
return "";
}
buffer << stream.rdbuf();
std::string s = buffer.str();
return s;
}
std::string SonarWebSocketImpl::absoluteFilePath(const char* filename) {
return std::string(deviceData_.privateAppDirectory + "/sonar/" + filename);
}
bool SonarWebSocketImpl::ensureSonarDirExists() {
std::string dirPath = absoluteFilePath("");
struct stat info;
if (stat(dirPath.c_str(), &info) != 0) {
int ret = mkdir(dirPath.c_str(), S_IRUSR | S_IWUSR | S_IXUSR);
return ret == 0;
} else if (info.st_mode & S_IFDIR) {
return true;
} else {
SONAR_LOG(std::string(
"ERROR: Sonar path exists but is not a directory: " + dirPath)
.c_str());
return false;
}
}
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarInitConfig.h>
#include <Sonar/SonarWebSocket.h>
#include <folly/Executor.h>
#include <folly/io/async/EventBase.h>
#include <rsocket/RSocket.h>
#include <mutex>
namespace facebook {
namespace sonar {
class ConnectionEvents;
class Responder;
class SonarWebSocketImpl : public SonarWebSocket {
friend ConnectionEvents;
friend Responder;
public:
SonarWebSocketImpl(SonarInitConfig config);
~SonarWebSocketImpl();
void start() override;
void stop() override;
bool isOpen() const override;
void setCallbacks(Callbacks* callbacks) override;
void sendMessage(const folly::dynamic& message) override;
void reconnect();
private:
bool isOpen_ = false;
Callbacks* callbacks_;
DeviceData deviceData_;
folly::EventBase* worker_;
std::unique_ptr<rsocket::RSocketClient> client_;
bool connectionIsTrusted_;
int failedConnectionAttempts_ = 0;
void startSync();
void doCertificateExchange();
void connectSecurely();
std::string loadCSRFromFile();
std::string loadStringFromFile(std::string fileName);
std::string absoluteFilePath(const char* relativeFilePath);
bool isCertificateExchangeNeeded();
void requestSignedCertFromSonar();
bool ensureSonarDirExists();
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarConnection.h>
#include <map>
#include <string>
namespace facebook {
namespace sonar {
class SonarConnectionMock : public SonarConnection {
public:
void send(const std::string& method, const folly::dynamic& params) override {
sent_[method] = params;
}
void receive(const std::string& method, const SonarReceiver& receiver)
override {
receivers_[method] = receiver;
}
std::map<std::string, folly::dynamic> sent_;
std::map<std::string, SonarReceiver> receivers_;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarPlugin.h>
namespace facebook {
namespace sonar {
namespace test {
class SonarPluginMock : public SonarPlugin {
using ConnectionCallback =
std::function<void(std::shared_ptr<SonarConnection>)>;
using DisconnectionCallback = std::function<void()>;
public:
SonarPluginMock(const std::string& identifier) : identifier_(identifier) {}
SonarPluginMock(
const std::string& identifier,
const ConnectionCallback& connectionCallback)
: identifier_(identifier), connectionCallback_(connectionCallback) {}
SonarPluginMock(
const std::string& identifier,
const ConnectionCallback& connectionCallback,
const DisconnectionCallback& disconnectionCallback)
: identifier_(identifier),
connectionCallback_(connectionCallback),
disconnectionCallback_(disconnectionCallback) {}
std::string identifier() const override {
return identifier_;
}
void didConnect(std::shared_ptr<SonarConnection> conn) override {
if (connectionCallback_) {
connectionCallback_(conn);
}
}
void didDisconnect() override {
if (disconnectionCallback_) {
disconnectionCallback_();
}
}
private:
std::string identifier_;
ConnectionCallback connectionCallback_;
DisconnectionCallback disconnectionCallback_;
};
} // namespace test
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarResponder.h>
#include <folly/json.h>
#include <vector>
namespace facebook {
namespace sonar {
class SonarResponderMock : public SonarResponder {
public:
SonarResponderMock(
std::vector<folly::dynamic>* successes = nullptr,
std::vector<folly::dynamic>* errors = nullptr)
: successes_(successes), errors_(errors) {}
void success(const folly::dynamic& response) const override {
if (successes_) {
successes_->push_back(response);
}
}
void error(const folly::dynamic& response) const override {
if (errors_) {
errors_->push_back(response);
}
}
private:
std::vector<folly::dynamic>* successes_;
std::vector<folly::dynamic>* errors_;
};
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#pragma once
#include <Sonar/SonarWebSocket.h>
namespace facebook {
namespace sonar {
namespace test {
class SonarWebSocketMock : public SonarWebSocket {
public:
SonarWebSocketMock() : callbacks(nullptr) {}
void start() override {
open = true;
if (callbacks) {
callbacks->onConnected();
}
}
void stop() override {
open = false;
if (callbacks) {
callbacks->onDisconnected();
}
}
bool isOpen() const override {
return open;
}
void sendMessage(const folly::dynamic& message) override {
messages.push_back(message);
}
void setCallbacks(Callbacks* aCallbacks) override {
callbacks = aCallbacks;
}
public:
bool open = false;
Callbacks* callbacks;
std::vector<folly::dynamic> messages;
};
} // namespace test
} // namespace sonar
} // namespace facebook

View File

@@ -0,0 +1,292 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
#include <Sonar/SonarClient.h>
#include <SonarTestLib/SonarPluginMock.h>
#include <SonarTestLib/SonarWebSocketMock.h>
#include <folly/json.h>
#include <gtest/gtest.h>
namespace facebook {
namespace sonar {
namespace test {
using folly::dynamic;
TEST(SonarClientTests, testSaneMocks) {
SonarWebSocketMock socket;
socket.start();
EXPECT_TRUE(socket.isOpen());
socket.stop();
EXPECT_FALSE(socket.isOpen());
SonarPluginMock plugin("Test");
EXPECT_EQ(plugin.identifier(), "Test");
}
TEST(SonarClientTests, testGetPlugins) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
client.start();
client.addPlugin(std::make_shared<SonarPluginMock>("Cat"));
client.addPlugin(std::make_shared<SonarPluginMock>("Dog"));
dynamic message = dynamic::object("id", 1)("method", "getPlugins");
socket->callbacks->onMessageReceived(message);
dynamic expected = dynamic::object("id", 1)(
"success", dynamic::object("plugins", dynamic::array("Cat", "Dog")));
EXPECT_EQ(socket->messages.back(), expected);
}
TEST(SonarClientTests, testGetPlugin) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
const auto catPlugin = std::make_shared<SonarPluginMock>("Cat");
client.addPlugin(catPlugin);
const auto dogPlugin = std::make_shared<SonarPluginMock>("Dog");
client.addPlugin(dogPlugin);
EXPECT_EQ(catPlugin, client.getPlugin("Cat"));
EXPECT_EQ(dogPlugin, client.getPlugin("Dog"));
}
TEST(SonarClientTests, testGetPluginWithDowncast) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
const auto catPlugin = std::make_shared<SonarPluginMock>("Cat");
client.addPlugin(catPlugin);
EXPECT_EQ(catPlugin, client.getPlugin<SonarPluginMock>("Cat"));
}
TEST(SonarClientTests, testRemovePlugin) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
client.start();
auto plugin = std::make_shared<SonarPluginMock>("Test");
client.addPlugin(plugin);
client.removePlugin(plugin);
dynamic message = dynamic::object("id", 1)("method", "getPlugins");
socket->callbacks->onMessageReceived(message);
dynamic expected = dynamic::object("id", 1)(
"success", dynamic::object("plugins", dynamic::array()));
EXPECT_EQ(socket->messages.back(), expected);
}
TEST(SonarClientTests, testStartStop) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
client.start();
EXPECT_TRUE(socket->isOpen());
client.stop();
EXPECT_FALSE(socket->isOpen());
}
TEST(SonarClientTests, testConnectDisconnect) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
bool pluginConnected = false;
const auto connectionCallback = [&](std::shared_ptr<SonarConnection> conn) {
pluginConnected = true;
};
const auto disconnectionCallback = [&]() { pluginConnected = false; };
auto plugin = std::make_shared<SonarPluginMock>("Test", connectionCallback,
disconnectionCallback);
client.addPlugin(plugin);
client.start();
dynamic messageInit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageInit);
EXPECT_TRUE(pluginConnected);
client.stop();
EXPECT_FALSE(pluginConnected);
}
TEST(SonarClientTests, testInitDeinit) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
bool pluginConnected = false;
const auto connectionCallback = [&](std::shared_ptr<SonarConnection> conn) {
pluginConnected = true;
};
const auto disconnectionCallback = [&]() { pluginConnected = false; };
auto plugin = std::make_shared<SonarPluginMock>("Test", connectionCallback,
disconnectionCallback);
client.start();
client.addPlugin(plugin);
EXPECT_FALSE(pluginConnected);
dynamic expected = dynamic::object("method", "refreshPlugins");
EXPECT_EQ(socket->messages.front(), expected);
dynamic messageInit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageInit);
EXPECT_TRUE(pluginConnected);
dynamic messageDeinit = dynamic::object("method", "deinit")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageDeinit);
EXPECT_FALSE(pluginConnected);
dynamic messageReinit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageReinit);
EXPECT_TRUE(pluginConnected);
client.stop();
EXPECT_FALSE(pluginConnected);
}
TEST(SonarClientTests, testRemovePluginWhenConnected) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
bool pluginConnected = false;
const auto connectionCallback = [&](std::shared_ptr<SonarConnection> conn) {
pluginConnected = true;
};
const auto disconnectionCallback = [&]() { pluginConnected = false; };
auto plugin = std::make_shared<SonarPluginMock>("Test", connectionCallback,
disconnectionCallback);
client.addPlugin(plugin);
client.start();
client.removePlugin(plugin);
EXPECT_FALSE(pluginConnected);
dynamic expected = dynamic::object("method", "refreshPlugins");
EXPECT_EQ(socket->messages.back(), expected);
}
TEST(SonarClientTests, testUnhandleableMethod) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
auto plugin = std::make_shared<SonarPluginMock>("Test");
client.addPlugin(plugin);
dynamic messageInit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageInit);
dynamic messageExecute = dynamic::object("id", 1)("method", "unexpected");
socket->callbacks->onMessageReceived(messageExecute);
dynamic expected = dynamic::object("id", 1)(
"error",
dynamic::object("message", "Received unknown method: unexpected"));
EXPECT_EQ(socket->messages.back(), expected);
}
TEST(SonarClientTests, testExecute) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
client.start();
const auto connectionCallback = [](std::shared_ptr<SonarConnection> conn) {
const auto receiver = [](const dynamic &params,
std::unique_ptr<SonarResponder> responder) {
dynamic payload = dynamic::object("message", "yes_i_hear_u");
responder->success(payload);
};
conn->receive("plugin_can_u_hear_me", receiver);
};
auto plugin = std::make_shared<SonarPluginMock>("Test", connectionCallback);
client.addPlugin(plugin);
dynamic messageInit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageInit);
dynamic messageUnexpected = dynamic::object("id", 1)("method", "execute")(
"params",
dynamic::object("api", "Test")("method", "plugin_can_u_hear_me"));
socket->callbacks->onMessageReceived(messageUnexpected);
dynamic expected = dynamic::object("id", 1)(
"success", dynamic::object("message", "yes_i_hear_u"));
EXPECT_EQ(socket->messages.back(), expected);
}
TEST(SonarClientTests, testExecuteWithParams) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
const auto connectionCallback = [&](std::shared_ptr<SonarConnection> conn) {
const auto receiver = [](const dynamic &params,
std::unique_ptr<SonarResponder> responder) {
const auto &first = params["first"].asString();
const auto &second = params["second"].asString();
std::map<std::string, std::string> m{{"dog", "woof"}, {"cat", "meow"}};
dynamic payload = dynamic::object(first, m[first])(second, m[second]);
responder->success(payload);
};
conn->receive("animal_sounds", receiver);
};
auto plugin = std::make_shared<SonarPluginMock>("Test", connectionCallback);
client.addPlugin(plugin);
dynamic messageInit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Test"));
socket->callbacks->onMessageReceived(messageInit);
dynamic messageExecute = dynamic::object("id", 1)("method", "execute")(
"params",
dynamic::object("api", "Test")("method", "animal_sounds")(
"params", dynamic::object("first", "dog")("second", "cat")));
socket->callbacks->onMessageReceived(messageExecute);
dynamic expected = dynamic::object("id", 1)(
"success", dynamic::object("dog", "woof")("cat", "meow"));
EXPECT_EQ(socket->messages.back(), expected);
}
TEST(SonarClientTests, testExceptionUnknownPlugin) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
client.start();
dynamic messageInit = dynamic::object("method", "init")(
"params", dynamic::object("plugin", "Unknown"));
socket->callbacks->onMessageReceived(messageInit);
EXPECT_EQ(socket->messages.back()["error"]["message"],
"plugin Unknown not found for method init");
}
TEST(SonarClientTests, testExceptionUnknownApi) {
auto socket = new SonarWebSocketMock;
SonarClient client(std::unique_ptr<SonarWebSocketMock>{socket});
client.start();
dynamic messageInit = dynamic::object("method", "execute")(
"params", dynamic::object("api", "Unknown"));
socket->callbacks->onMessageReceived(messageInit);
EXPECT_EQ(socket->messages.back()["error"]["message"],
"connection Unknown not found for method execute");
}
} // namespace test
} // namespace sonar
} // namespace facebook

37
xplat/build.gradle Normal file
View File

@@ -0,0 +1,37 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.compileSdkVersion
buildToolsVersion rootProject.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
externalNativeBuild {
cmake {
arguments '-DANDROID_TOOLCHAIN=clang'
}
}
}
externalNativeBuild {
cmake {
path './CMakeLists.txt'
}
}
sourceSets {
main {
manifest.srcFile './AndroidManifest.xml'
}
}
dependencies {
implementation project(':easywsclient')
implementation project(':folly')
}
}