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 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 15 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
6 changes: 6 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,12 @@
.PollingBaseUrl(default_endpoints.PollingBaseUrl())
.StreamingBaseUrl(default_endpoints.StreamingBaseUrl());

if (in.proxy) {
if (in.proxy->httpProxy) {
config_builder.HttpProperties().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 @@ -421,6 +421,28 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b,
char const* key,
char const* value);

/**
* Specifies an HTTP proxy which the client 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 b Client config builder. Must not be NULL.
* @param http_proxy HTTP proxy URL. Must not be NULL.
*/
LD_EXPORT(void)
LDClientConfigBuilder_HttpProperties_HttpProxy(LDClientConfigBuilder b,
char const* http_proxy);

/**
* Sets the TLS options builder. The builder is automatically freed.
*
Expand Down
9 changes: 9 additions & 0 deletions libs/client-sdk/src/bindings/c/builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,15 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b,
TO_BUILDER(b)->HttpProperties().Header(key, value);
}

LD_EXPORT(void)
LDClientConfigBuilder_HttpProperties_HttpProxy(LDClientConfigBuilder b,
char const* http_proxy) {
LD_ASSERT_NOT_NULL(b);
LD_ASSERT_NOT_NULL(http_proxy);

TO_BUILDER(b)->HttpProperties().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_.HttpProxy()) {
client_builder.http_proxy(*http_config_.HttpProxy());
}

auto weak_self = weak_from_this();

client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) {
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 @@ -179,9 +180,32 @@ class HttpPropertiesBuilder {
HttpPropertiesBuilder& Tls(TlsBuilder<SDK> builder);

/**
* Build a set of HttpProperties.
* @return The built properties.
* 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>
HttpPropertiesBuilder& HttpProxy(std::string http_proxy) {
http_proxy_ = std::move(http_proxy);
return *this;
}

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

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

} // namespace launchdarkly::config::shared::builders
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class HttpProperties final {
std::chrono::milliseconds write_timeout,
std::chrono::milliseconds response_timeout,
std::map<std::string, std::string> base_headers,
TlsOptions tls);
TlsOptions tls,
std::optional<std::string> http_proxy);

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

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

[[nodiscard]] std::optional<std::string> HttpProxy() 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.
std::optional<std::string> http_proxy_;
};

bool operator==(HttpProperties const& lhs, HttpProperties const& rhs);
Expand Down
6 changes: 4 additions & 2 deletions libs/common/include/launchdarkly/config/shared/defaults.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ struct Defaults<ClientSDK> {
std::chrono::seconds{10},
std::chrono::seconds{10},
std::map<std::string, std::string>(),
TLS()};
TLS(),
std::nullopt};
}

static auto StreamingConfig() -> shared::built::StreamingConfig<ClientSDK> {
Expand Down Expand Up @@ -105,7 +106,8 @@ struct Defaults<ServerSDK> {
std::chrono::seconds{10},
std::chrono::seconds{10},
std::map<std::string, std::string>(),
TLS()};
TLS(),
std::nullopt};
}

static auto StreamingConfig() -> built::StreamingConfig<ServerSDK> {
Expand Down
20 changes: 13 additions & 7 deletions libs/common/src/config/http_properties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@

namespace launchdarkly::config::shared::built {

TlsOptions::TlsOptions(TlsOptions::VerifyMode verify_mode,
TlsOptions::TlsOptions(VerifyMode const verify_mode,
std::optional<std::string> ca_bundle_path)
: verify_mode_(verify_mode), ca_bundle_path_(std::move(ca_bundle_path)) {}

TlsOptions::TlsOptions(TlsOptions::VerifyMode verify_mode)
TlsOptions::TlsOptions(VerifyMode const verify_mode)
: TlsOptions(verify_mode, std::nullopt) {}

TlsOptions::TlsOptions()
: TlsOptions(TlsOptions::VerifyMode::kVerifyPeer, std::nullopt) {}
TlsOptions::TlsOptions() : TlsOptions(VerifyMode::kVerifyPeer, std::nullopt) {}

TlsOptions::VerifyMode TlsOptions::PeerVerifyMode() const {
return verify_mode_;
Expand All @@ -27,13 +26,15 @@ HttpProperties::HttpProperties(std::chrono::milliseconds connect_timeout,
std::chrono::milliseconds write_timeout,
std::chrono::milliseconds response_timeout,
std::map<std::string, std::string> base_headers,
TlsOptions tls)
TlsOptions tls,
std::optional<std::string> http_proxy)
: connect_timeout_(connect_timeout),
read_timeout_(read_timeout),
write_timeout_(write_timeout),
response_timeout_(response_timeout),
base_headers_(std::move(base_headers)),
tls_(std::move(tls)) {}
tls_(std::move(tls)),
http_proxy_(std::move(http_proxy)) {}

std::chrono::milliseconds HttpProperties::ConnectTimeout() const {
return connect_timeout_;
Expand All @@ -59,11 +60,16 @@ TlsOptions const& HttpProperties::Tls() const {
return tls_;
}

std::optional<std::string> HttpProperties::HttpProxy() const {
return http_proxy_;
}

bool operator==(HttpProperties const& lhs, HttpProperties const& rhs) {
return lhs.ReadTimeout() == rhs.ReadTimeout() &&
lhs.WriteTimeout() == rhs.WriteTimeout() &&
lhs.ConnectTimeout() == rhs.ConnectTimeout() &&
lhs.BaseHeaders() == rhs.BaseHeaders() && lhs.Tls() == rhs.Tls();
lhs.BaseHeaders() == rhs.BaseHeaders() && lhs.Tls() == rhs.Tls() &&
lhs.HttpProxy() == rhs.HttpProxy();
}

bool operator==(TlsOptions const& lhs, TlsOptions const& rhs) {
Expand Down
34 changes: 25 additions & 9 deletions libs/common/src/config/http_properties_builder.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#include <utility>

#include <launchdarkly/config/shared/builders/http_properties_builder.hpp>
#include <launchdarkly/config/shared/defaults.hpp>
#include <launchdarkly/config/shared/sdks.hpp>

#include <cstring>

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

Expand Down Expand Up @@ -51,6 +50,7 @@ HttpPropertiesBuilder<SDK>::HttpPropertiesBuilder(
response_timeout_ = properties.ResponseTimeout();
base_headers_ = properties.BaseHeaders();
tls_ = properties.Tls();
http_proxy_ = properties.HttpProxy();
}

template <typename SDK>
Expand Down Expand Up @@ -121,17 +121,33 @@ HttpPropertiesBuilder<SDK>& HttpPropertiesBuilder<SDK>::Tls(
return *this;
}

static std::optional<std::string> HttpProxyFromEnv() {
if (char const* http_proxy = std::getenv("http_proxy")) {
if (strlen(http_proxy) > 0) {
return http_proxy;
}
}
return std::nullopt;
}

template <typename SDK>
built::HttpProperties HttpPropertiesBuilder<SDK>::Build() const {
std::map<std::string, std::string> headers = base_headers_;

if (!wrapper_name_.empty()) {
std::map<std::string, std::string> headers_with_wrapper(base_headers_);
headers_with_wrapper["X-LaunchDarkly-Wrapper"] =
headers["X-LaunchDarkly-Wrapper"] =
wrapper_name_ + "/" + wrapper_version_;
return {connect_timeout_, read_timeout_, write_timeout_,
response_timeout_, headers_with_wrapper, tls_.Build()};
}
return {connect_timeout_, read_timeout_, write_timeout_,
response_timeout_, base_headers_, tls_.Build()};

std::optional<std::string> http_proxy;

if constexpr (std::is_same_v<SDK, ClientSDK>) {
http_proxy = http_proxy_.has_value() ? http_proxy_ : HttpProxyFromEnv();
}

return {connect_timeout_, read_timeout_, write_timeout_,
response_timeout_, std::move(headers), tls_.Build(),
http_proxy};
}

template class TlsBuilder<config::shared::ClientSDK>;
Expand Down
Loading
Loading