Skip to content

Commit

Permalink
Add support for HTTP tunnelling (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
leemaguire authored Nov 27, 2023
1 parent 54230f1 commit edd14c2
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 37 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ X.Y.Z Release notes (YYYY-MM-DD)
Subsequently, if you can make a frozen Realm / Object live again by calling `thaw()`.
It is not recommended to have too many long-lived frozen Realm's / Objects in your application as it may balloon memory consumption.
* Add ability to sort `experimental::results` / `managed<std::vector<T>>`.
* Add support for HTTP tunneling. Usage:
```cpp
realm::proxy_config proxy_config;
proxy_config.type = realm::proxy_config_type::HTTP;
proxy_config.port = 8080;
proxy_config.address = "127.0.0.1";
proxy_config.username_password = {"username", "password"};

realm::App::configuration app_config;
app_config.proxy_configuration = proxy_config;
auto app = realm::App(app_config);

auto user = app.get_current_user();
auto sync_config = user->flexible_sync_configuration();
sync_config.set_proxy_config(proxy_config);
auto synced_realm = experimental::db(sync_config);
```
### Breaking Changes
* None
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,4 @@ endif()

if (NOT REALM_CPP_NO_TESTS)
add_subdirectory(tests)
endif()
endif()
3 changes: 2 additions & 1 deletion src/cpprealm/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ namespace realm {

auto app_config = app::App::Config();
app_config.app_id = config.app_id;
app_config.transport = std::make_shared<internal::DefaultTransport>(config.custom_http_headers);
app_config.transport = std::make_shared<internal::DefaultTransport>(config.custom_http_headers,
config.proxy_configuration);
app_config.base_url = config.base_url;
auto device_info = app::App::Config::DeviceInfo();

Expand Down
2 changes: 2 additions & 0 deletions src/cpprealm/app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <utility>

namespace realm {
using proxy_config = sync_config::proxy_config;
using sync_session = internal::bridge::sync_session;
class SyncUser;

Expand Down Expand Up @@ -211,6 +212,7 @@ class App {
std::optional<std::string> path;
std::optional<std::map<std::string, std::string>> custom_http_headers;
std::optional<std::array<char, 64>> metadata_encryption_key;
std::optional<sync_config::proxy_config> proxy_configuration;
};

[[deprecated("Use App(const configuration&) instead.")]]
Expand Down
1 change: 1 addition & 0 deletions src/cpprealm/db.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ namespace realm {
using sync_config = internal::bridge::realm::sync_config;
using db_config = internal::bridge::realm::config;
using sync_session = internal::bridge::sync_session;
using status = internal::bridge::status;

template <typename ...Ts>
struct db {
Expand Down
32 changes: 31 additions & 1 deletion src/cpprealm/internal/apple/network_transport.mm
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@

#include <Foundation/NSData.h>
#include <Foundation/NSURL.h>
#include <Foundation/NSURLCache.h>
#include <Foundation/NSURLResponse.h>
#include <Foundation/NSURLSession.h>

#include <realm/util/base64.hpp>

namespace realm::internal {
void DefaultTransport::send_request_to_server(const app::Request& request,
util::UniqueFunction<void(const app::Response&)>&& completion_block) {
Expand Down Expand Up @@ -63,7 +66,34 @@
[urlRequest setHTTPBody:[[NSString stringWithCString:request.body.c_str() encoding:NSUTF8StringEncoding]
dataUsingEncoding:NSUTF8StringEncoding]];
}
NSURLSession *session = [NSURLSession sharedSession];

NSURLSession *session;
if (m_proxy_config) {
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSString *proxyHost = @(m_proxy_config->address.c_str());
NSInteger proxyPort = m_proxy_config->port;
sessionConfiguration.connectionProxyDictionary = @{
@"HTTPSEnable": @YES,
@"HTTPSProxy": @(proxyPort),
@"HTTPSPort": proxyHost,
};
sessionConfiguration.requestCachePolicy = NSURLRequestCachePolicy::NSURLRequestReloadIgnoringLocalCacheData;
urlRequest.cachePolicy = NSURLRequestCachePolicy::NSURLRequestReloadIgnoringLocalCacheData;
[[NSURLCache sharedURLCache] removeAllCachedResponses];

session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
if (m_proxy_config->username_password) {
auto userpass = util::format("%1:%2", m_proxy_config->username_password->first, m_proxy_config->username_password->second);
std::string encoded_userpass;
encoded_userpass.resize(realm::util::base64_encoded_size(userpass.length()));
realm::util::base64_encode(userpass.data(), userpass.size(), encoded_userpass.data(), encoded_userpass.size());
[urlRequest addValue:@(util::format("Basic %1", encoded_userpass).c_str()) forHTTPHeaderField:@"'Proxy-Authorization'"];
}

} else {
session = [NSURLSession sharedSession];
}

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest
completionHandler:[request = std::move(request),
completion_ptr = completion_block.release()](NSData *data, NSURLResponse *response, NSError *error) {
Expand Down
12 changes: 12 additions & 0 deletions src/cpprealm/internal/bridge/realm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,18 @@ namespace realm::internal::bridge {
}
}

void realm::config::set_proxy_config(const sync_config::proxy_config& config) {
if (get_config()->sync_config) {
SyncConfig::ProxyConfig core_config;
core_config.address = config.address;
core_config.port = config.port;
core_config.type = SyncConfig::ProxyConfig::Type::HTTP;
get_config()->sync_config->proxy_config = std::move(core_config);
} else {
throw std::logic_error("Proxy configuration can only be set on a config for a synced Realm.");
}
}

void realm::config::set_schema_version(uint64_t version) {
get_config()->schema_version = version;
}
Expand Down
39 changes: 24 additions & 15 deletions src/cpprealm/internal/bridge/realm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,29 @@ namespace realm::internal::bridge {
RecoverOrDiscard,
};

struct sync_config;
struct sync_config {

struct proxy_config {
using port_type = std::uint_fast16_t;
std::string address;
port_type port;
// For basic authorization.
std::optional<std::pair<std::string, std::string>> username_password;
};

struct flx_sync_enabled {};
sync_config() {}
sync_config(const std::shared_ptr<SyncUser> &user);
sync_config(const std::shared_ptr<SyncConfig> &);//NOLINT(google-explicit-constructor)
operator std::shared_ptr<SyncConfig>() const; //NOLINT(google-explicit-constructor)
void set_client_resync_mode(client_resync_mode &&);
void set_stop_policy(sync_session_stop_policy &&);
void set_error_handler(std::function<void(const sync_session &, const sync_error &)> &&fn);

private:
std::shared_ptr<SyncConfig> m_config;
};

struct config {
// How to handle update_schema() being called on a file which has
// already been initialized with a different schema
Expand Down Expand Up @@ -151,6 +173,7 @@ namespace realm::internal::bridge {
void set_scheduler(const std::shared_ptr<struct scheduler>&);
void set_sync_config(const std::optional<struct sync_config>&);
void set_custom_http_headers(const std::map<std::string, std::string>& headers);
void set_proxy_config(const sync_config::proxy_config&);
void set_schema_version(uint64_t version);
void set_encryption_key(const std::array<char, 64>&);
std::optional<schema> get_schema();
Expand All @@ -164,20 +187,6 @@ namespace realm::internal::bridge {
#endif
};

struct sync_config {
struct flx_sync_enabled {};
sync_config() {}
sync_config(const std::shared_ptr<SyncUser> &user);
sync_config(const std::shared_ptr<SyncConfig> &);//NOLINT(google-explicit-constructor)
operator std::shared_ptr<SyncConfig>() const; //NOLINT(google-explicit-constructor)
void set_client_resync_mode(client_resync_mode &&);
void set_stop_policy(sync_session_stop_policy &&);
void set_error_handler(std::function<void(const sync_session &, const sync_error &)> &&fn);

private:
std::shared_ptr<SyncConfig> m_config;
};

realm();
realm(const config&); //NOLINT(google-explicit-constructor)
realm(std::shared_ptr<Realm>); //NOLINT(google-explicit-constructor)
Expand Down
14 changes: 11 additions & 3 deletions src/cpprealm/internal/curl/network_transport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ namespace realm::internal {
return nitems * size;
}

static app::Response do_http_request(const app::Request& request)
static app::Response do_http_request(const app::Request& request,
const std::optional<bridge::realm::sync_config::proxy_config>& proxy_config = std::nullopt)
{
CurlGlobalGuard curl_global_guard;
auto curl = curl_easy_init();
Expand All @@ -104,6 +105,13 @@ namespace realm::internal {
data. */
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());

if (proxy_config) {
curl_easy_setopt(curl, CURLOPT_PROXY, util::format("%1:%2", proxy_config->address, proxy_config->port).c_str());
curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, 1L);
if (proxy_config->username_password) {}
curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, util::format("%1:%2", proxy_config->username_password->first, proxy_config->username_password->second).c_str());
}

/* Now specify the POST data */
if (request.method == app::HttpMethod::post) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
Expand Down Expand Up @@ -161,10 +169,10 @@ namespace realm::internal {
if (m_custom_http_headers) {
auto req_copy = request;
req_copy.headers.insert(m_custom_http_headers->begin(), m_custom_http_headers->end());
completion_block(do_http_request(req_copy));
completion_block(do_http_request(req_copy, m_proxy_config));
return;
}
completion_block(do_http_request(request));
completion_block(do_http_request(request, m_proxy_config));
}


Expand Down
9 changes: 8 additions & 1 deletion src/cpprealm/internal/generic_network_transport.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,25 @@
#define realm_cpp_generic_network_transport

#include <realm/object-store/sync/generic_network_transport.hpp>
#include <cpprealm/internal/bridge/realm.hpp>

#include <map>
#include <optional>

namespace realm::internal {

class DefaultTransport : public app::GenericNetworkTransport {
public:
DefaultTransport(const std::optional<std::map<std::string, std::string>>& custom_http_headers = std::nullopt) : m_custom_http_headers(custom_http_headers) {}
DefaultTransport(const std::optional<std::map<std::string, std::string>>& custom_http_headers = std::nullopt,
const std::optional<bridge::realm::sync_config::proxy_config>& proxy_config = std::nullopt)
: m_custom_http_headers(custom_http_headers),
m_proxy_config(proxy_config) {}

void send_request_to_server(const app::Request& request,
util::UniqueFunction<void(const app::Response&)>&& completion);
private:
std::optional<std::map<std::string, std::string>> m_custom_http_headers;
std::optional<bridge::realm::sync_config::proxy_config> m_proxy_config;
};

} // namespace realm
Expand Down
89 changes: 74 additions & 15 deletions src/cpprealm/internal/network/network_transport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
#include <realm/sync/network/http.hpp>
#include <realm/sync/network/network.hpp>
#include <realm/sync/noinst/client_impl_base.hpp>
#include <realm/util/base64.hpp>
#include <realm/util/uri.hpp>

#include <regex>


namespace realm::internal {
struct DefaultSocket : realm::sync::network::Socket {
Expand Down Expand Up @@ -80,31 +85,81 @@ namespace realm::internal {

void DefaultTransport::send_request_to_server(const app::Request& request,
util::UniqueFunction<void(const app::Response&)>&& completion_block) {
// Get the host name.
size_t found = request.url.find_first_of(":");
std::string domain = request.url.substr(found+3);
size_t found1 = domain.find_first_of(":");
size_t found2 = domain.find_first_of("/");
const std::string host = domain.substr(found1 + 1, found2 - found1 - 1);
std::string host = realm::util::Uri(request.url).get_auth();

realm::sync::network::Service service;
realm::sync::network::Endpoint ep;
using namespace realm::sync::network::ssl;
Context m_ssl_context;
DefaultSocket socket{service};

try {
realm::sync::network::Endpoint ep;
auto resolver = realm::sync::network::Resolver{service};
auto resolved = resolver.resolve(sync::network::Resolver::Query(host, "443"));
ep = *resolved.begin();
if (m_proxy_config) {
std::string proxy_address = realm::util::Uri(m_proxy_config->address).get_auth();
if (proxy_address.empty()) {
std::error_code e;
auto address = realm::sync::network::make_address(m_proxy_config->address, e);
if (e.value() > 0) {
app::Response response;
response.custom_status_code = e.value();
response.body = e.message();
completion_block(std::move(response));
return;
}
ep = realm::sync::network::Endpoint(address, m_proxy_config->port);
} else {
auto resolved = resolver.resolve(sync::network::Resolver::Query(proxy_address, m_proxy_config->port));
ep = *resolved.begin();
}
} else {
auto resolved = resolver.resolve(sync::network::Resolver::Query(host, "443"));
ep = *resolved.begin();
}
socket.connect(ep);
} catch (...) {
app::Response response;
response.http_status_code = 500;
response.custom_status_code = util::error::operation_aborted;
completion_block(std::move(response));
return;
}

using namespace realm::sync::network::ssl;
Context m_ssl_context;
DefaultSocket socket{service};
socket.connect(ep);
auto logger = util::Logger::get_default_logger();

if (m_proxy_config) {
realm::sync::HTTPRequest req;
req.method = realm::sync::HTTPMethod::Connect;
req.headers.emplace("Host", util::format("%1:%2", host, "443"));
if (m_proxy_config->username_password) {
auto userpass = util::format("%1:%2", m_proxy_config->username_password->first, m_proxy_config->username_password->second);
std::string encoded_userpass;
encoded_userpass.resize(realm::util::base64_encoded_size(userpass.length()));
realm::util::base64_encode(userpass.data(), userpass.size(), encoded_userpass.data(), encoded_userpass.size());
req.headers.emplace("Proxy-Authorization", util::format("Basic %1", encoded_userpass));
}
realm::sync::HTTPClient<DefaultSocket> m_proxy_client = realm::sync::HTTPClient<DefaultSocket>(socket, logger);
auto handler = [&](realm::sync::HTTPResponse response, std::error_code ec) {
if (ec && ec != util::error::operation_aborted) {
app::Response res;
res.custom_status_code = util::error::operation_aborted;
completion_block(std::move(res));
return;
}

if (response.status != realm::sync::HTTPStatus::Ok) {
app::Response res;
res.http_status_code = static_cast<uint16_t>(response.status);
completion_block(std::move(res));
return;
}
service.stop();
};

m_proxy_client.async_request(req, std::move(handler)); // Throws

service.run_until_stopped();
service.reset();
}

#if REALM_INCLUDE_CERTS
m_ssl_context.use_included_certificate_roots();
Expand All @@ -114,7 +169,6 @@ namespace realm::internal {
socket.ssl_stream->set_host_name(host); // Throws

socket.ssl_stream->set_verify_mode(VerifyMode::peer);
auto logger = util::Logger::get_default_logger();
socket.ssl_stream->set_logger(logger.get());

realm::sync::HTTPHeaders headers;
Expand Down Expand Up @@ -182,6 +236,11 @@ namespace realm::internal {
res.custom_status_code = 0;
cb(res);
});
} else {
app::Response response;
response.custom_status_code = util::error::operation_aborted;
completion_block(std::move(response));
return;
}
};
socket.async_handshake(std::move(handler)); // Throws
Expand Down

0 comments on commit edd14c2

Please sign in to comment.