Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support HTTP proxy in client SDK #467

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions contract-tests/client-contract-tests/src/entity_manager.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "entity_manager.hpp"

Check failure on line 1 in contract-tests/client-contract-tests/src/entity_manager.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/client-contract-tests/src/entity_manager.cpp:1:10 [clang-diagnostic-error]

'entity_manager.hpp' file not found

#include <launchdarkly/config/client.hpp>
#include <launchdarkly/context_builder.hpp>
Expand All @@ -17,7 +17,7 @@
logger_{logger} {}

static tl::expected<launchdarkly::Context, launchdarkly::JsonError>
ParseContext(nlohmann::json value) {

Check warning on line 20 in contract-tests/client-contract-tests/src/entity_manager.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/client-contract-tests/src/entity_manager.cpp:20:1 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'ParseContext' is non-const and globally accessible, consider making it const
auto boost_json_val = boost::json::parse(value.dump());
return boost::json::value_to<
tl::expected<launchdarkly::Context, launchdarkly::JsonError>>(
Expand All @@ -38,6 +38,13 @@
.PollingBaseUrl(default_endpoints.PollingBaseUrl())
.StreamingBaseUrl(default_endpoints.StreamingBaseUrl());

if (in.proxy) {
if (in.proxy->httpProxy) {
config_builder.HttpProperties().Proxy(
ProxyBuilder().HttpProxy(*in.proxy->httpProxy));
}
}

if (in.serviceEndpoints) {
if (in.serviceEndpoints->streaming) {
endpoints.StreamingBaseUrl(*in.serviceEndpoints->streaming);
Expand Down
3 changes: 2 additions & 1 deletion contract-tests/client-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "server.hpp"

Check failure on line 1 in contract-tests/client-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/client-contract-tests/src/main.cpp:1:10 [clang-diagnostic-error]

'server.hpp' file not found

#include <launchdarkly/logging/console_backend.hpp>

Expand All @@ -18,7 +18,7 @@
using launchdarkly::LogLevel;

int main(int argc, char* argv[]) {
launchdarkly::Logger logger{

Check warning on line 21 in contract-tests/client-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/client-contract-tests/src/main.cpp:21:26 [cppcoreguidelines-init-variables]

variable 'logger' is not initialized
std::make_unique<ConsoleBackend>("client-contract-tests")};

std::string const default_port = "8123";
Expand All @@ -31,8 +31,8 @@
try {
net::io_context ioc{1};

auto p = boost::lexical_cast<unsigned short>(port);

Check warning on line 34 in contract-tests/client-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/client-contract-tests/src/main.cpp:34:14 [readability-identifier-length]

variable name 'p' is too short, expected at least 3 characters
server srv(ioc, "0.0.0.0", p, logger);

Check warning on line 35 in contract-tests/client-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/client-contract-tests/src/main.cpp:35:16 [cppcoreguidelines-init-variables]

variable 'srv' is not initialized

srv.add_capability("client-side");
srv.add_capability("mobile");
Expand All @@ -46,7 +46,8 @@
srv.add_capability("tls:verify-peer");
srv.add_capability("tls:skip-verify-peer");
srv.add_capability("tls:custom-ca");

srv.add_capability("http-proxy");

net::signal_set signals{ioc, SIGINT, SIGTERM};

boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable {
Expand Down
10 changes: 9 additions & 1 deletion contract-tests/data-model/include/data_model/data_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include <optional>
#include <string>
#include <unordered_map>
#include "nlohmann/json.hpp"

Check failure on line 6 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:6:10 [clang-diagnostic-error]

'nlohmann/json.hpp' file not found

namespace nlohmann {
template <typename T>
Expand Down Expand Up @@ -37,6 +37,12 @@
skipVerifyPeer,
customCAFile);

struct ConfigProxyParams {
std::optional<std::string> httpProxy;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigProxyParams, httpProxy);

struct ConfigStreamingParams {
std::optional<std::string> baseUri;
std::optional<uint32_t> initialRetryDelayMs;
Expand All @@ -59,7 +65,7 @@
pollIntervalMs,
filter);

struct ConfigEventParams {

Check warning on line 68 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:68:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: globalPrivateAttributes
std::optional<std::string> baseUri;
std::optional<uint32_t> capacity;
std::optional<bool> enableDiagnostics;
Expand Down Expand Up @@ -87,7 +93,7 @@
polling,
events);

struct ConfigClientSideParams {

Check warning on line 96 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:96:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: initialContext
nlohmann::json initialContext;
std::optional<bool> evaluationReasons;
std::optional<bool> useReport;
Expand Down Expand Up @@ -118,6 +124,7 @@
std::optional<ConfigClientSideParams> clientSide;
std::optional<ConfigTags> tags;
std::optional<ConfigTLSParams> tls;
std::optional<ConfigProxyParams> proxy;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
Expand All @@ -130,9 +137,10 @@
serviceEndpoints,
clientSide,
tags,
tls);
tls,
proxy);

struct ContextSingleParams {

Check warning on line 143 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:143:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: custom
std::optional<std::string> kind;
std::string key;
std::optional<std::string> name;
Expand Down Expand Up @@ -227,7 +235,7 @@
defaultValue,
detail);

struct EvaluateFlagResponse {

Check warning on line 238 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:238:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: value, reason
nlohmann::json value;
std::optional<uint32_t> variationIndex;
std::optional<nlohmann::json> reason;
Expand All @@ -238,7 +246,7 @@
variationIndex,
reason);

struct EvaluateAllFlagParams {

Check warning on line 249 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:249:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: context
std::optional<nlohmann::json> context;
std::optional<bool> withReasons;
std::optional<bool> clientSideOnly;
Expand All @@ -251,7 +259,7 @@
clientSideOnly,
detailsOnlyForTrackedFlags);

struct EvaluateAllFlagsResponse {

Check warning on line 262 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:262:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: state
nlohmann::json state;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ typedef struct _LDPersistenceCustomBuilder* LDPersistenceCustomBuilder;
typedef struct _LDClientHttpPropertiesTlsBuilder*
LDClientHttpPropertiesTlsBuilder;

typedef struct _LDClientHttpPropertiesProxyBuilder*
LDClientHttpPropertiesProxyBuilder;

typedef void (*SetFn)(char const* storage_namespace,
char const* key,
char const* data,
Expand Down Expand Up @@ -422,15 +425,71 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b,
char const* value);

/**
* Sets the TLS options builder. The builder is automatically freed.
* Creates a new ProxyBuilder for the HttpProperties builder.
*
* If not passed into the HttpProperties
* builder, must be manually freed with LDClientHttpPropertiesProxyBuilder_Free.
*
* @return New builder for proxy options.
*/
LD_EXPORT(LDClientHttpPropertiesProxyBuilder)
LDClientHttpPropertiesProxyBuilder_New(void);

/**
* Frees a ProxyBuilder. Do not call if the builder was consumed by
* the HttpProperties builder.
*
* @param b Builder to free.
*/
LD_EXPORT(void)
LDClientHttpPropertiesProxyBuilder_Free(LDClientHttpPropertiesProxyBuilder b);

/**
* Specifies an HTTP proxy which the client should use for any HTTP requests.
*
* This setting affects streaming mode, polling mode, and event delivery.
* The argument should be of the form: 'http://proxy.example.com:8080'.
*
* The scheme must be 'http' and the port is optional (80 if not
* specified.)
*
* The SDK respects the 'http_proxy' environment variable as an alternative
* to this method. If both are set, this method takes precedence.
*
* @param b Client config builder. Must not be NULL.
* @param http_proxy HTTP proxy URL. Must not be NULL.
*/

LD_EXPORT(void)
LDClientHttpPropertiesProxyBuilder_HttpProxy(
LDClientHttpPropertiesProxyBuilder b,
char const* http_proxy);

/**
* Sets the ProxyBuilder. The builder is automatically freed.
*
* WARNING: Do not call any other LDClientHttpPropertiesProxyBuilder function on
* the provided LDClientHttpPropertiesProxyBuilder after calling this function.
* It is undefined behavior.
*
* @param b Client config builder. Must not be NULL.
* @param proxy_builder The ProxyBuilder. Must not be NULL.
*/
LD_EXPORT(void)
LDClientConfigBuilder_HttpProperties_Proxy(
LDClientConfigBuilder b,
LDClientHttpPropertiesProxyBuilder proxy_builder);

/**
* Sets the TlsBuilder. The builder is automatically freed.
*
* WARNING: Do not call any other
* LDClientHttpPropertiesTlsBuilder function on the provided
* LDClientHttpPropertiesTlsBuilder after calling this function.
* It is undefined behavior.
*
* @param b Client config builder. Must not be NULL.
* @param tls_builder The TLS options builder. Must not be NULL.
* @param tls_builder The TlsBuilder. Must not be NULL.
*/
LD_EXPORT(void)
LDClientConfigBuilder_HttpProperties_Tls(
Expand Down
37 changes: 37 additions & 0 deletions libs/client-sdk/src/bindings/c/builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ using namespace launchdarkly::client_side;
#define FROM_TLS_BUILDER(ptr) \
(reinterpret_cast<LDClientHttpPropertiesTlsBuilder>(ptr))

#define TO_PROXY_BUILDER(ptr) (reinterpret_cast<ProxyBuilder*>(ptr))

#define FROM_PROXY_BUILDER(ptr) \
(reinterpret_cast<LDClientHttpPropertiesProxyBuilder>(ptr))

class PersistenceImplementationWrapper : public IPersistence {
public:
explicit PersistenceImplementationWrapper(LDPersistence impl)
Expand Down Expand Up @@ -311,6 +316,38 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b,
TO_BUILDER(b)->HttpProperties().Header(key, value);
}

LD_EXPORT(LDClientHttpPropertiesProxyBuilder)
LDClientHttpPropertiesProxyBuilder_New(void) {
return FROM_PROXY_BUILDER(new ProxyBuilder());
}

LD_EXPORT(void)
LDClientHttpPropertiesProxyBuilder_Free(LDClientHttpPropertiesProxyBuilder b) {
delete TO_PROXY_BUILDER(b);
}

LD_EXPORT(void)
LDClientConfigBuilder_HttpProperties_Proxy(
LDClientConfigBuilder b,
LDClientHttpPropertiesProxyBuilder proxy_builder) {
LD_ASSERT_NOT_NULL(b);
LD_ASSERT_NOT_NULL(proxy_builder);

TO_BUILDER(b)->HttpProperties().Proxy(*TO_PROXY_BUILDER(proxy_builder));

LDClientHttpPropertiesProxyBuilder_Free(proxy_builder);
}

LD_EXPORT(void)
LDClientHttpPropertiesProxyBuilder_HttpProxy(
LDClientHttpPropertiesProxyBuilder b,
char const* http_proxy) {
LD_ASSERT_NOT_NULL(b);
LD_ASSERT_NOT_NULL(http_proxy);

TO_PROXY_BUILDER(b)->HttpProxy(http_proxy);
}

LD_EXPORT(void)
LDClientConfigBuilder_HttpProperties_Tls(
LDClientConfigBuilder b,
Expand Down
4 changes: 4 additions & 0 deletions libs/client-sdk/src/data_sources/streaming_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ void StreamingDataSource::Start() {
client_builder.custom_ca_file(*ca_file);
}

if (http_config_.Proxy().Http()) {
client_builder.http_proxy(*http_config_.Proxy().Http());
}

auto weak_self = weak_from_this();

client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) {
Expand Down
35 changes: 34 additions & 1 deletion libs/client-sdk/tests/client_config_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,15 @@ TEST(ClientConfigBindings, AllConfigs) {
LDClientHttpPropertiesTlsBuilder_New();
LDClientHttpPropertiesTlsBuilder_Free(tls_builder2);

LDClientConfigBuilder_Logging_Disable(builder);
LDClientHttpPropertiesProxyBuilder proxy_builder =
LDClientHttpPropertiesProxyBuilder_New();
LDClientHttpPropertiesProxyBuilder_HttpProxy(proxy_builder,
"http://proxy:8080");
LDClientConfigBuilder_HttpProperties_Proxy(builder, proxy_builder);

LDClientHttpPropertiesProxyBuilder proxy_builder2 =
LDClientHttpPropertiesProxyBuilder_New();
LDClientHttpPropertiesProxyBuilder_Free(proxy_builder2);

LDLoggingBasicBuilder log_builder = LDLoggingBasicBuilder_New();
LDLoggingBasicBuilder_Level(log_builder, LD_LOG_WARN);
Expand Down Expand Up @@ -263,3 +271,28 @@ TEST(ClientConfigBindings, CustomLogger) {
ASSERT_EQ(args.write->level, LD_LOG_ERROR);
ASSERT_EQ(args.write->msg, "hello");
}

TEST(ClientConfigBindings, ProxyOptions) {
using namespace launchdarkly;

LDClientConfigBuilder builder = LDClientConfigBuilder_New("sdk-123");

LDClientHttpPropertiesProxyBuilder proxy_builder =
LDClientHttpPropertiesProxyBuilder_New();

LDClientHttpPropertiesProxyBuilder_HttpProxy(proxy_builder,
"http://proxy.com:8080");

LDClientConfigBuilder_HttpProperties_Proxy(builder, proxy_builder);

LDClientConfig config = nullptr;
LDStatus status = LDClientConfigBuilder_Build(builder, &config);
ASSERT_TRUE(LDStatus_Ok(status));

auto client_config = reinterpret_cast<client_side::Config*>(config);

ASSERT_EQ(client_config->HttpProperties().Proxy().Http(),
"http://proxy.com:8080");

LDClientConfig_Free(config);
}
1 change: 1 addition & 0 deletions libs/common/include/launchdarkly/config/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ using DataSourceBuilder = config::shared::builders::DataSourceBuilder<SDK>;
using LoggingBuilder = config::shared::builders::LoggingBuilder;
using PersistenceBuilder = config::shared::builders::PersistenceBuilder<SDK>;
using TlsBuilder = config::shared::builders::TlsBuilder<SDK>;
using ProxyBuilder = config::shared::builders::ProxyBuilder<SDK>;

using Config = config::Config<SDK>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <vector>

#include <launchdarkly/config/shared/built/http_properties.hpp>
#include <launchdarkly/config/shared/sdks.hpp>

namespace launchdarkly::config::shared::builders {

Expand Down Expand Up @@ -65,6 +66,48 @@ class TlsBuilder {
enum built::TlsOptions::VerifyMode verify_mode_;
std::optional<std::string> custom_ca_file_;
};

template <typename SDK>
class ProxyBuilder {
public:
ProxyBuilder();

ProxyBuilder(built::ProxyOptions const& proxy);

/**
* NOTE: This method and the associated 'http_proxy' environment variable
* are only available for the client-side SDK.
*
* Specifies an HTTP proxy which the SDK should use to communicate
* with LaunchDarkly.
*
* SDK <-- HTTP, plaintext --> Proxy <-- HTTPS --> LaunchDarkly
*
* This setting affects streaming mode, polling mode, and event delivery.
* The argument should be of the form: 'http://proxy.example.com:8080'.
*
* The scheme must be 'http' and the port is optional (80 if not
* specified.)
*
* The SDK respects the 'http_proxy' environment variable as an alternative
* to this method. If both are set, this method takes precedence.
*
* @param http_proxy HTTP proxy URL.
*/
template <typename T = SDK,
std::enable_if_t<std::is_same_v<T, ClientSDK>, int> = 0>
ProxyBuilder& HttpProxy(std::string http_proxy) {
http_proxy_ = std::move(http_proxy);
return *this;
}

[[nodiscard]] built::ProxyOptions Build() const;

private:
std::optional<std::string> http_proxy_;
std::optional<std::string> https_proxy_;
};

/**
* Class used for building a set of HttpProperties.
* @tparam SDK The SDK type to build properties for. This affects the default
Expand Down Expand Up @@ -179,9 +222,12 @@ class HttpPropertiesBuilder {
HttpPropertiesBuilder& Tls(TlsBuilder<SDK> builder);

/**
* Build a set of HttpProperties.
* @return The built properties.
*
* @param builder Sets the builder for proxy properties.
* @return A reference to this builder.
*/
HttpPropertiesBuilder& Proxy(ProxyBuilder<SDK> builder);

[[nodiscard]] built::HttpProperties Build() const;

private:
Expand All @@ -193,6 +239,7 @@ class HttpPropertiesBuilder {
std::string wrapper_version_;
std::map<std::string, std::string> base_headers_;
TlsBuilder<SDK> tls_;
ProxyBuilder<SDK> proxy_;
};

} // namespace launchdarkly::config::shared::builders
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,29 @@ class TlsOptions final {
std::optional<std::string> ca_bundle_path_;
};

class ProxyOptions final {
public:
ProxyOptions(std::optional<std::string> http_proxy,
std::optional<std::string> https_proxy);

ProxyOptions() = default;
[[nodiscard]] std::optional<std::string> Http() const;
[[nodiscard]] std::optional<std::string> Https() const;

private:
std::optional<std::string> http_proxy_;
std::optional<std::string> https_proxy_;
};

class HttpProperties final {
public:
HttpProperties(std::chrono::milliseconds connect_timeout,
std::chrono::milliseconds read_timeout,
std::chrono::milliseconds write_timeout,
std::chrono::milliseconds response_timeout,
std::map<std::string, std::string> base_headers,
TlsOptions tls);
TlsOptions tls,
ProxyOptions proxy);

[[nodiscard]] std::chrono::milliseconds ConnectTimeout() const;
[[nodiscard]] std::chrono::milliseconds ReadTimeout() const;
Expand All @@ -41,18 +56,20 @@ class HttpProperties final {

[[nodiscard]] TlsOptions const& Tls() const;

[[nodiscard]] ProxyOptions const& Proxy() const;

private:
std::chrono::milliseconds connect_timeout_;
std::chrono::milliseconds read_timeout_;
std::chrono::milliseconds write_timeout_;
std::chrono::milliseconds response_timeout_;
std::map<std::string, std::string> base_headers_;
TlsOptions tls_;

// TODO: Proxy.
ProxyOptions proxy_;
};

bool operator==(HttpProperties const& lhs, HttpProperties const& rhs);
bool operator==(ProxyOptions const& lhs, ProxyOptions const& rhs);
bool operator==(TlsOptions const& lhs, TlsOptions const& rhs);

} // namespace launchdarkly::config::shared::built
Loading
Loading