diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 8a70bebf4..1cf7c7fda 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -29,7 +29,7 @@ jobs: run_tests: false - name: 'Launch test service as background task' run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & - - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.0 + - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.2 with: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index ea7fdc4bb..e6eead4f0 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -28,7 +28,7 @@ jobs: run_tests: false - name: 'Launch test service as background task' run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & - - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.0 + - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.2 with: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} diff --git a/contract-tests/client-contract-tests/src/entity_manager.cpp b/contract-tests/client-contract-tests/src/entity_manager.cpp index 9993b932b..f094af4f5 100644 --- a/contract-tests/client-contract-tests/src/entity_manager.cpp +++ b/contract-tests/client-contract-tests/src/entity_manager.cpp @@ -129,6 +129,14 @@ std::optional EntityManager::create(ConfigParams const& in) { } } + if (in.tls) { + auto builder = TlsBuilder(); + if (in.tls->skipVerifyPeer) { + builder.SkipVerifyPeer(*in.tls->skipVerifyPeer); + } + config_builder.HttpProperties().Tls(std::move(builder)); + } + auto config = config_builder.Build(); if (!config) { LD_LOG(logger_, LogLevel::kWarn) diff --git a/contract-tests/client-contract-tests/src/main.cpp b/contract-tests/client-contract-tests/src/main.cpp index decf36e1d..b85f35cea 100644 --- a/contract-tests/client-contract-tests/src/main.cpp +++ b/contract-tests/client-contract-tests/src/main.cpp @@ -21,7 +21,7 @@ int main(int argc, char* argv[]) { launchdarkly::Logger logger{ std::make_unique("client-contract-tests")}; - const std::string default_port = "8123"; + std::string const default_port = "8123"; std::string port = default_port; if (argc == 2) { port = @@ -43,6 +43,8 @@ int main(int argc, char* argv[]) { srv.add_capability("client-independence"); srv.add_capability("inline-context"); srv.add_capability("anonymous-redaction"); + srv.add_capability("tls:verify-peer"); + srv.add_capability("tls:skip-verify-peer"); net::signal_set signals{ioc, SIGINT, SIGTERM}; diff --git a/contract-tests/data-model/include/data_model/data_model.hpp b/contract-tests/data-model/include/data_model/data_model.hpp index b4baccb9b..b89346bd8 100644 --- a/contract-tests/data-model/include/data_model/data_model.hpp +++ b/contract-tests/data-model/include/data_model/data_model.hpp @@ -29,6 +29,12 @@ struct adl_serializer> { }; } // namespace nlohmann +struct ConfigTLSParams { + std::optional skipVerifyPeer; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTLSParams, + skipVerifyPeer); + struct ConfigStreamingParams { std::optional baseUri; std::optional initialRetryDelayMs; @@ -98,6 +104,7 @@ struct ConfigParams { std::optional serviceEndpoints; std::optional clientSide; std::optional tags; + std::optional tls; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, credential, @@ -108,7 +115,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams, events, serviceEndpoints, clientSide, - tags); + tags, + tls); struct ContextSingleParams { std::optional kind; diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index fa9d2579c..cd3a5c60d 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -120,6 +120,14 @@ std::optional EntityManager::create(ConfigParams const& in) { } } + if (in.tls) { + auto builder = config::builders::TlsBuilder(); + if (in.tls->skipVerifyPeer) { + builder.SkipVerifyPeer(*in.tls->skipVerifyPeer); + } + config_builder.HttpProperties().Tls(std::move(builder)); + } + auto config = config_builder.Build(); if (!config) { LD_LOG(logger_, LogLevel::kWarn) diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp index 0cd11d688..8fe0da565 100644 --- a/contract-tests/server-contract-tests/src/main.cpp +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -21,7 +21,7 @@ int main(int argc, char* argv[]) { launchdarkly::Logger logger{ std::make_unique("server-contract-tests")}; - const std::string default_port = "8123"; + std::string const default_port = "8123"; std::string port = default_port; if (argc == 2) { port = @@ -42,6 +42,8 @@ int main(int argc, char* argv[]) { srv.add_capability("server-side-polling"); srv.add_capability("inline-context"); srv.add_capability("anonymous-redaction"); + srv.add_capability("tls:verify-peer"); + srv.add_capability("tls:skip-verify-peer"); net::signal_set signals{ioc, SIGINT, SIGTERM}; diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h index d1f92375e..b3a9190a4 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h @@ -21,6 +21,8 @@ typedef struct _LDClientConfigBuilder* LDClientConfigBuilder; typedef struct _LDDataSourceStreamBuilder* LDDataSourceStreamBuilder; typedef struct _LDDataSourcePollBuilder* LDDataSourcePollBuilder; typedef struct _LDPersistenceCustomBuilder* LDPersistenceCustomBuilder; +typedef struct _LDClientHttpPropertiesTlsBuilder* + LDClientHttpPropertiesTlsBuilder; typedef void (*SetFn)(char const* storage_namespace, char const* key, @@ -333,7 +335,6 @@ LD_EXPORT(void) LDDataSourceStreamBuilder_Free(LDDataSourceStreamBuilder b); * * @return New builder for Polling method. */ - LD_EXPORT(LDDataSourcePollBuilder) LDDataSourcePollBuilder_New(); @@ -390,6 +391,51 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b, char const* key, char const* value); +/** + * Sets the TLS options builder. The builder is consumed; do not free it. + * @param b Client config builder. Must not be NULL. + * @param tls_builder The TLS options builder. Must not be NULL. + */ +LD_EXPORT(void) +LDClientConfigBuilder_HttpProperties_Tls( + LDClientConfigBuilder b, + LDClientHttpPropertiesTlsBuilder tls_builder); + +/** + * Creates a new TLS options builder for the HttpProperties builder. + * + * If not passed into the HttpProperties + * builder, must be manually freed with LDClientHttpPropertiesTlsBuilder_Free. + * + * @return New builder for TLS options. + */ +LD_EXPORT(LDClientHttpPropertiesTlsBuilder) +LDClientHttpPropertiesTlsBuilder_New(void); + +/** + * Frees a TLS options builder. Do not call if the builder was consumed by + * the HttpProperties builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDClientHttpPropertiesTlsBuilder_Free(LDClientHttpPropertiesTlsBuilder b); + +/** + * Configures TLS peer certificate verification. Peer verification + * is enabled by default. + * + * Disabling peer verification is not recommended unless a specific + * use-case calls for it. + * + * @param b Client config builder. Must not be NULL. + * @param skip_verify_peer True to skip verification. + */ +LD_EXPORT(void) +LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer( + LDClientHttpPropertiesTlsBuilder b, + bool skip_verify_peer); + /** * Disables the default SDK logging. * @param b Client config builder. Must not be NULL. diff --git a/libs/client-sdk/src/bindings/c/builder.cpp b/libs/client-sdk/src/bindings/c/builder.cpp index 0259ba83b..f1071a73d 100644 --- a/libs/client-sdk/src/bindings/c/builder.cpp +++ b/libs/client-sdk/src/bindings/c/builder.cpp @@ -41,6 +41,11 @@ using namespace launchdarkly::client_side; #define FROM_CUSTOM_PERSISTENCE_BUILDER(ptr) \ (reinterpret_cast(ptr)) +#define TO_TLS_BUILDER(ptr) (reinterpret_cast(ptr)) + +#define FROM_TLS_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + class PersistenceImplementationWrapper : public IPersistence { public: explicit PersistenceImplementationWrapper(LDPersistence impl) @@ -306,6 +311,37 @@ LDClientConfigBuilder_HttpProperties_Header(LDClientConfigBuilder b, TO_BUILDER(b)->HttpProperties().Header(key, value); } +LD_EXPORT(void) +LDClientConfigBuilder_HttpProperties_Tls( + LDClientConfigBuilder b, + LDClientHttpPropertiesTlsBuilder tls_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(tls_builder); + + TO_BUILDER(b)->HttpProperties().Tls(*TO_TLS_BUILDER(tls_builder)); + + LDClientHttpPropertiesTlsBuilder_Free(tls_builder); +} + +LD_EXPORT(void) +LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer( + LDClientHttpPropertiesTlsBuilder b, + bool skip_verify_peer) { + LD_ASSERT_NOT_NULL(b); + + TO_TLS_BUILDER(b)->SkipVerifyPeer(skip_verify_peer); +} + +LD_EXPORT(LDClientHttpPropertiesTlsBuilder) +LDClientHttpPropertiesTlsBuilder_New(void) { + return FROM_TLS_BUILDER(new TlsBuilder()); +} + +LD_EXPORT(void) +LDClientHttpPropertiesTlsBuilder_Free(LDClientHttpPropertiesTlsBuilder b) { + delete TO_TLS_BUILDER(b); +} + LD_EXPORT(void) LDClientConfigBuilder_Logging_Disable(LDClientConfigBuilder b) { LD_ASSERT_NOT_NULL(b); diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index 4f685c2b2..5f0481a47 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -74,7 +74,7 @@ PollingDataSource::PollingDataSource( status_manager_(status_manager), data_source_handler_( DataSourceEventHandler(context, handler, logger, status_manager_)), - requester_(ioc), + requester_(ioc, http_properties.Tls().PeerVerifyMode()), timer_(ioc), polling_interval_( std::get< @@ -88,6 +88,10 @@ PollingDataSource::PollingDataSource( auto const& polling_config = std::get< config::shared::built::PollingConfig>( data_source_config.method); + if (http_properties.Tls().PeerVerifyMode() == + config::shared::built::TlsOptions::VerifyMode::kVerifyNone) { + LD_LOG(logger_, LogLevel::kDebug) << "TLS peer verification disabled"; + } if (polling_interval_ < polling_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval too frequent, defaulting to " diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index fd8c4e29a..9b2e169d2 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -127,6 +127,11 @@ void StreamingDataSource::Start() { client_builder.header(header.first, header.second); } + if (http_config_.Tls().PeerVerifyMode() == + config::shared::built::TlsOptions::VerifyMode::kVerifyNone) { + client_builder.skip_verify_peer(true); + } + auto weak_self = weak_from_this(); client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { diff --git a/libs/client-sdk/tests/client_config_test.cpp b/libs/client-sdk/tests/client_config_test.cpp index f9b923afe..c62357541 100644 --- a/libs/client-sdk/tests/client_config_test.cpp +++ b/libs/client-sdk/tests/client_config_test.cpp @@ -65,14 +65,29 @@ TEST(ClientConfigBindings, AllConfigs) { LDDataSourceStreamBuilder_InitialReconnectDelayMs(stream_builder, 500); LDClientConfigBuilder_DataSource_MethodStream(builder, stream_builder); + LDDataSourceStreamBuilder stream_builder2 = LDDataSourceStreamBuilder_New(); + LDDataSourceStreamBuilder_Free(stream_builder2); + LDDataSourcePollBuilder poll_builder = LDDataSourcePollBuilder_New(); LDDataSourcePollBuilder_IntervalS(poll_builder, 10); LDClientConfigBuilder_DataSource_MethodPoll(builder, poll_builder); + LDDataSourcePollBuilder poll_builder2 = LDDataSourcePollBuilder_New(); + LDDataSourcePollBuilder_Free(poll_builder2); + LDClientConfigBuilder_HttpProperties_Header(builder, "foo", "bar"); LDClientConfigBuilder_HttpProperties_WrapperName(builder, "wrapper"); LDClientConfigBuilder_HttpProperties_WrapperVersion(builder, "v1.2.3"); + LDClientHttpPropertiesTlsBuilder tls_builder = + LDClientHttpPropertiesTlsBuilder_New(); + LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer(tls_builder, false); + LDClientConfigBuilder_HttpProperties_Tls(builder, tls_builder); + + LDClientHttpPropertiesTlsBuilder tls_builder2 = + LDClientHttpPropertiesTlsBuilder_New(); + LDClientHttpPropertiesTlsBuilder_Free(tls_builder2); + LDClientConfigBuilder_Logging_Disable(builder); LDLoggingBasicBuilder log_builder = LDLoggingBasicBuilder_New(); diff --git a/libs/common/include/launchdarkly/config/client.hpp b/libs/common/include/launchdarkly/config/client.hpp index 516c4b85b..dd288a82a 100644 --- a/libs/common/include/launchdarkly/config/client.hpp +++ b/libs/common/include/launchdarkly/config/client.hpp @@ -22,6 +22,7 @@ using HttpPropertiesBuilder = using DataSourceBuilder = config::shared::builders::DataSourceBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; using PersistenceBuilder = config::shared::builders::PersistenceBuilder; +using TlsBuilder = config::shared::builders::TlsBuilder; using Config = config::Config; diff --git a/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp index 2ccdef725..6c7334ec9 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp @@ -10,6 +10,45 @@ namespace launchdarkly::config::shared::builders { +/** + * Class used for building TLS options used within HttpProperties. + * @tparam SDK The SDK type to build options for. This affects the default + * values of the built options. + */ +template +class TlsBuilder { + public: + /** + * Construct a new TlsBuilder. The builder will use the default + * properties based on the SDK type. Setting a property will override + * the default value. + */ + TlsBuilder(); + + /** + * Create a TLS builder from an initial set of options. + * This can be useful when extending a set of options for a request. + * + * @param tls The TLS options to start with. + */ + TlsBuilder(built::TlsOptions const& tls); + + /** + * Whether to skip verifying the remote peer's certificates. + * @param skip_verify_peer True to skip verification, false to verify. + * @return A reference to this builder. + */ + TlsBuilder& SkipVerifyPeer(bool skip_verify_peer); + + /** + * Builds the TLS options. + * @return The built options. + */ + [[nodiscard]] built::TlsOptions Build() const; + + private: + enum built::TlsOptions::VerifyMode verify_mode_; +}; /** * Class used for building a set of HttpProperties. * @tparam SDK The SDK type to build properties for. This affects the default @@ -116,6 +155,13 @@ class HttpPropertiesBuilder { HttpPropertiesBuilder& Header(std::string key, std::optional value); + /** + * Sets the builder for TLS properties. + * @param builder The TLS property builder. + * @return A reference to this builder. + */ + HttpPropertiesBuilder& Tls(TlsBuilder builder); + /** * Build a set of HttpProperties. * @return The built properties. @@ -130,6 +176,7 @@ class HttpPropertiesBuilder { std::string wrapper_name_; std::string wrapper_version_; std::map base_headers_; + TlsBuilder tls_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp b/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp index d077e18f4..ac24267c1 100644 --- a/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp @@ -7,13 +7,25 @@ namespace launchdarkly::config::shared::built { +class TlsOptions final { + public: + enum class VerifyMode { kVerifyPeer, kVerifyNone }; + TlsOptions(VerifyMode verify_mode); + TlsOptions(); + [[nodiscard]] VerifyMode PeerVerifyMode() const; + + private: + VerifyMode verify_mode_; +}; + 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 base_headers); + std::map base_headers, + TlsOptions tls); [[nodiscard]] std::chrono::milliseconds ConnectTimeout() const; [[nodiscard]] std::chrono::milliseconds ReadTimeout() const; @@ -22,16 +34,20 @@ class HttpProperties final { [[nodiscard]] std::chrono::milliseconds ResponseTimeout() const; [[nodiscard]] std::map const& BaseHeaders() const; + [[nodiscard]] TlsOptions const& Tls() const; + private: std::chrono::milliseconds connect_timeout_; std::chrono::milliseconds read_timeout_; std::chrono::milliseconds write_timeout_; std::chrono::milliseconds response_timeout_; std::map base_headers_; + TlsOptions tls_; // TODO: Proxy. }; bool operator==(HttpProperties const& lhs, HttpProperties const& rhs); +bool operator==(TlsOptions const& lhs, TlsOptions const& rhs); } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 9b7814360..0995eaa52 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -49,10 +49,15 @@ struct Defaults { std::nullopt}; } + static auto TLS() -> shared::built::TlsOptions { return {}; } + static auto HttpProperties() -> shared::built::HttpProperties { - return {std::chrono::seconds{10}, std::chrono::seconds{10}, - std::chrono::seconds{10}, std::chrono::seconds{10}, - std::map()}; + return {std::chrono::seconds{10}, + std::chrono::seconds{10}, + std::chrono::seconds{10}, + std::chrono::seconds{10}, + std::map(), + TLS()}; } static auto StreamingConfig() -> shared::built::StreamingConfig { @@ -92,10 +97,15 @@ struct Defaults { 1000}; } + static auto TLS() -> shared::built::TlsOptions { return {}; } + static auto HttpProperties() -> built::HttpProperties { - return {std::chrono::seconds{10}, std::chrono::seconds{10}, - std::chrono::seconds{10}, std::chrono::seconds{10}, - std::map()}; + return {std::chrono::seconds{10}, + std::chrono::seconds{10}, + std::chrono::seconds{10}, + std::chrono::seconds{10}, + std::map(), + TLS()}; } static auto StreamingConfig() -> built::StreamingConfig { diff --git a/libs/common/src/config/http_properties.cpp b/libs/common/src/config/http_properties.cpp index 905db4292..ffdc77d63 100644 --- a/libs/common/src/config/http_properties.cpp +++ b/libs/common/src/config/http_properties.cpp @@ -4,16 +4,27 @@ namespace launchdarkly::config::shared::built { +TlsOptions::TlsOptions(enum TlsOptions::VerifyMode verify_mode) + : verify_mode_(verify_mode) {} + +TlsOptions::TlsOptions() : TlsOptions(TlsOptions::VerifyMode::kVerifyPeer) {} + +TlsOptions::VerifyMode TlsOptions::PeerVerifyMode() const { + return verify_mode_; +} + HttpProperties::HttpProperties(std::chrono::milliseconds connect_timeout, std::chrono::milliseconds read_timeout, std::chrono::milliseconds write_timeout, std::chrono::milliseconds response_timeout, - std::map base_headers) + std::map base_headers, + TlsOptions tls) : connect_timeout_(connect_timeout), read_timeout_(read_timeout), write_timeout_(write_timeout), response_timeout_(response_timeout), - base_headers_(std::move(base_headers)) {} + base_headers_(std::move(base_headers)), + tls_(std::move(tls)) {} std::chrono::milliseconds HttpProperties::ConnectTimeout() const { return connect_timeout_; @@ -35,11 +46,19 @@ std::map const& HttpProperties::BaseHeaders() const { return base_headers_; } +TlsOptions const& HttpProperties::Tls() const { + return tls_; +} + 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.BaseHeaders() == rhs.BaseHeaders() && lhs.Tls() == rhs.Tls(); +} + +bool operator==(TlsOptions const& lhs, TlsOptions const& rhs) { + return lhs.PeerVerifyMode() == rhs.PeerVerifyMode(); } } // namespace launchdarkly::config::shared::built diff --git a/libs/common/src/config/http_properties_builder.cpp b/libs/common/src/config/http_properties_builder.cpp index 7fad6195f..96767ff9c 100644 --- a/libs/common/src/config/http_properties_builder.cpp +++ b/libs/common/src/config/http_properties_builder.cpp @@ -6,6 +6,27 @@ namespace launchdarkly::config::shared::builders { +template +TlsBuilder::TlsBuilder() : TlsBuilder(shared::Defaults::TLS()) {} + +template +TlsBuilder::TlsBuilder(built::TlsOptions const& tls) { + verify_mode_ = tls.PeerVerifyMode(); +} + +template +TlsBuilder& TlsBuilder::SkipVerifyPeer(bool skip_verify_peer) { + verify_mode_ = skip_verify_peer + ? built::TlsOptions::VerifyMode::kVerifyNone + : built::TlsOptions::VerifyMode::kVerifyPeer; + return *this; +} + +template +built::TlsOptions TlsBuilder::Build() const { + return {verify_mode_}; +} + template HttpPropertiesBuilder::HttpPropertiesBuilder() : HttpPropertiesBuilder(shared::Defaults::HttpProperties()) {} @@ -18,6 +39,7 @@ HttpPropertiesBuilder::HttpPropertiesBuilder( write_timeout_ = properties.WriteTimeout(); response_timeout_ = properties.ResponseTimeout(); base_headers_ = properties.BaseHeaders(); + tls_ = properties.Tls(); } template @@ -81,19 +103,29 @@ HttpPropertiesBuilder& HttpPropertiesBuilder::Header( return *this; } +template +HttpPropertiesBuilder& HttpPropertiesBuilder::Tls( + TlsBuilder builder) { + tls_ = std::move(builder); + return *this; +} + template built::HttpProperties HttpPropertiesBuilder::Build() const { if (!wrapper_name_.empty()) { std::map headers_with_wrapper(base_headers_); headers_with_wrapper["X-LaunchDarkly-Wrapper"] = wrapper_name_ + "/" + wrapper_version_; - return {connect_timeout_, read_timeout_, write_timeout_, - response_timeout_, headers_with_wrapper}; + 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_}; + return {connect_timeout_, read_timeout_, write_timeout_, + response_timeout_, base_headers_, tls_.Build()}; } +template class TlsBuilder; +template class TlsBuilder; + template class HttpPropertiesBuilder; template class HttpPropertiesBuilder; } // namespace launchdarkly::config::shared::builders diff --git a/libs/internal/include/launchdarkly/events/detail/request_worker.hpp b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp index 9046d49eb..232163edb 100644 --- a/libs/internal/include/launchdarkly/events/detail/request_worker.hpp +++ b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp @@ -9,6 +9,8 @@ #include #include +#include + #include namespace launchdarkly::events::detail { @@ -94,13 +96,16 @@ class RequestWorker { * @param retry_after How long to wait after a recoverable failure before * trying to deliver events again. * @param id Unique identifier for the flush worker (used for logging). + * @param mode TLS peer verification mode. * @param logger Logger. */ - RequestWorker(boost::asio::any_io_executor io, - std::chrono::milliseconds retry_after, - std::size_t id, - std::optional date_header_locale, - Logger& logger); + RequestWorker( + boost::asio::any_io_executor io, + std::chrono::milliseconds retry_after, + std::size_t id, + std::optional date_header_locale, + enum config::shared::built::TlsOptions::VerifyMode verify_mode, + Logger& logger); /** * Returns true if the worker is available for delivery. diff --git a/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp index a6a08441d..f83145c66 100644 --- a/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp +++ b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -29,11 +30,13 @@ class WorkerPool { * @param pool_size How many workers to make available. * @param delivery_retry_delay How long a worker should wait after a failed * delivery before trying again. + * @param verify_mode The TLS verification mode. * @param logger Logger. */ WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, std::chrono::milliseconds delivery_retry_delay, + enum config::shared::built::TlsOptions::VerifyMode verify_mode, Logger& logger); /** diff --git a/libs/internal/include/launchdarkly/network/asio_requester.hpp b/libs/internal/include/launchdarkly/network/asio_requester.hpp index 59da54c2b..0f8b23ac8 100644 --- a/libs/internal/include/launchdarkly/network/asio_requester.hpp +++ b/libs/internal/include/launchdarkly/network/asio_requester.hpp @@ -2,6 +2,7 @@ #include "http_requester.hpp" +#include #include #include @@ -29,6 +30,8 @@ using tcp = boost::asio::ip::tcp; namespace launchdarkly::network { +using VerifyMode = config::shared::built::TlsOptions::VerifyMode; + static unsigned char const kRedirectLimit = 20; static bool IsAbsolute(std::string_view str) { @@ -255,11 +258,14 @@ class AsioRequester { * must be accounted for. */ public: - AsioRequester(net::any_io_executor ctx) + AsioRequester(net::any_io_executor ctx, VerifyMode verify_mode) : ctx_(std::move(ctx)), ssl_ctx_(std::make_shared( launchdarkly::foxy::make_ssl_ctx(ssl::context::tlsv12_client))) { ssl_ctx_->set_default_verify_paths(); + ssl_ctx_->set_verify_mode(verify_mode == VerifyMode::kVerifyPeer + ? ssl::verify_peer + : ssl::verify_none); } template diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index e9cf2b525..e1f988529 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -47,6 +47,7 @@ AsioEventProcessor::AsioEventProcessor( workers_(io_, events_config.FlushWorkers(), events_config.DeliveryRetryDelay(), + http_properties.Tls().PeerVerifyMode(), logger), inbox_capacity_(events_config.Capacity()), inbox_size_(0), diff --git a/libs/internal/src/events/request_worker.cpp b/libs/internal/src/events/request_worker.cpp index b39090294..e5437777c 100644 --- a/libs/internal/src/events/request_worker.cpp +++ b/libs/internal/src/events/request_worker.cpp @@ -3,15 +3,17 @@ namespace launchdarkly::events::detail { -RequestWorker::RequestWorker(boost::asio::any_io_executor io, - std::chrono::milliseconds retry_after, - std::size_t id, - std::optional date_header_locale, - Logger& logger) +RequestWorker::RequestWorker( + boost::asio::any_io_executor io, + std::chrono::milliseconds retry_after, + std::size_t id, + std::optional date_header_locale, + enum config::shared::built::TlsOptions::VerifyMode verify_mode, + Logger& logger) : timer_(std::move(io)), retry_delay_(retry_after), state_(State::Idle), - requester_(timer_.get_executor()), + requester_(timer_.get_executor(), verify_mode), batch_(std::nullopt), tag_("flush-worker[" + std::to_string(id) + "]: "), date_header_locale_(std::move(date_header_locale)), diff --git a/libs/internal/src/events/worker_pool.cpp b/libs/internal/src/events/worker_pool.cpp index caa4a2b70..07a5df6a1 100644 --- a/libs/internal/src/events/worker_pool.cpp +++ b/libs/internal/src/events/worker_pool.cpp @@ -5,6 +5,8 @@ namespace launchdarkly::events::detail { +using namespace launchdarkly::config::shared::built; + std::optional GetLocale(std::string const& locale, std::string const& tag, Logger& logger) { @@ -22,6 +24,7 @@ std::optional GetLocale(std::string const& locale, WorkerPool::WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, std::chrono::milliseconds delivery_retry_delay, + enum TlsOptions::VerifyMode verify_mode, Logger& logger) : io_(io), workers_() { // The en_US.utf-8 locale is used whenever a date is parsed from the HTTP @@ -35,7 +38,8 @@ WorkerPool::WorkerPool(boost::asio::any_io_executor io, for (std::size_t i = 0; i < pool_size; i++) { workers_.emplace_back(std::make_unique( - io_, delivery_retry_delay, i, date_header_locale, logger)); + io_, delivery_retry_delay, i, date_header_locale, verify_mode, + logger)); } } diff --git a/libs/internal/tests/event_processor_test.cpp b/libs/internal/tests/event_processor_test.cpp index 226ebb3ef..2c718db1a 100644 --- a/libs/internal/tests/event_processor_test.cpp +++ b/libs/internal/tests/event_processor_test.cpp @@ -31,7 +31,8 @@ TEST(WorkerPool, PoolReturnsAvailableWorker) { auto work = boost::asio::make_work_guard(ioc); std::thread ioc_thread([&]() { ioc.run(); }); - WorkerPool pool(ioc.get_executor(), 1, std::chrono::seconds(1), logger); + WorkerPool pool(ioc.get_executor(), 1, std::chrono::seconds(1), + VerifyMode::kVerifyPeer, logger); RequestWorker* worker = pool.Get(boost::asio::use_future).get(); ASSERT_TRUE(worker); @@ -49,7 +50,8 @@ TEST(WorkerPool, PoolReturnsNullptrWhenNoWorkerAvaialable) { auto work = boost::asio::make_work_guard(ioc); std::thread ioc_thread([&]() { ioc.run(); }); - WorkerPool pool(ioc.get_executor(), 0, std::chrono::seconds(1), logger); + WorkerPool pool(ioc.get_executor(), 0, std::chrono::seconds(1), + VerifyMode::kVerifyPeer, logger); RequestWorker* worker = pool.Get(boost::asio::use_future).get(); ASSERT_FALSE(worker); diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h index c90af28e7..b1a368b9d 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h @@ -22,6 +22,8 @@ typedef struct _LDServerConfigBuilder* LDServerConfigBuilder; typedef struct _LDServerDataSourceStreamBuilder* LDServerDataSourceStreamBuilder; typedef struct _LDServerDataSourcePollBuilder* LDServerDataSourcePollBuilder; +typedef struct _LDServerHttpPropertiesTlsBuilder* + LDServerHttpPropertiesTlsBuilder; /** * Constructs a client-side config builder. @@ -117,8 +119,8 @@ LDServerConfigBuilder_Events_Enabled(LDServerConfigBuilder b, bool enabled); * that a previously-indexed context may cause generation of a redundant * index event. * @param b Server config builder. Must not be NULL. - * @param context_keys_capacity Maximum unique context keys to remember. The default - * is 1000. + * @param context_keys_capacity Maximum unique context keys to remember. The + * default is 1000. */ LD_EXPORT(void) LDServerConfigBuilder_Events_ContextKeysCapacity(LDServerConfigBuilder b, @@ -349,6 +351,51 @@ LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, char const* key, char const* value); +/** + * Sets the TLS options builder. The builder is consumed; do not free it. + * @param b Server config builder. Must not be NULL. + * @param tls_builder The TLS options builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_Tls( + LDServerConfigBuilder b, + LDServerHttpPropertiesTlsBuilder tls_builder); + +/** + * Creates a new TLS options builder for the HttpProperties builder. + * + * If not passed into the HttpProperties + * builder, must be manually freed with LDServerHttpPropertiesTlsBuilder_Free. + * + * @return New builder for TLS options. + */ +LD_EXPORT(LDServerHttpPropertiesTlsBuilder) +LDServerHttpPropertiesTlsBuilder_New(void); + +/** + * Frees a TLS options builder. Do not call if the builder was consumed by + * the HttpProperties builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDServerHttpPropertiesTlsBuilder_Free(LDServerHttpPropertiesTlsBuilder b); + +/** + * Configures TLS peer certificate verification. Peer verification + * is enabled by default. + * + * Disabling peer verification is not recommended unless a specific + * use-case calls for it. + * + * @param b Server config builder. Must not be NULL. + * @param skip_verify_peer True to skip verification. + */ +LD_EXPORT(void) +LDServerHttpPropertiesTlsBuilder_SkipVerifyPeer( + LDServerHttpPropertiesTlsBuilder b, + bool skip_verify_peer); + /** * Disables the default SDK logging. * @param b Server config builder. Must not be NULL. diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp index b767cc9b7..f49254431 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp @@ -23,5 +23,6 @@ using AppInfoBuilder = launchdarkly::config::shared::builders::AppInfoBuilder; using EventsBuilder = launchdarkly::config::shared::builders::EventsBuilder; using LoggingBuilder = launchdarkly::config::shared::builders::LoggingBuilder; +using TlsBuilder = launchdarkly::config::shared::builders::TlsBuilder; } // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index d8acfa485..eb9a0db75 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -48,6 +48,11 @@ using namespace launchdarkly::server_side::config::builders; #define FROM_CUSTOM_PERSISTENCE_BUILDER(ptr) \ (reinterpret_cast(ptr)) +#define TO_TLS_BUILDER(ptr) (reinterpret_cast(ptr)) + +#define FROM_TLS_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + LD_EXPORT(LDServerConfigBuilder) LDServerConfigBuilder_New(char const* sdk_key) { LD_ASSERT_NOT_NULL(sdk_key); @@ -332,6 +337,37 @@ LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, TO_BUILDER(b)->HttpProperties().Header(key, value); } +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_Tls( + LDServerConfigBuilder b, + LDServerHttpPropertiesTlsBuilder tls_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(tls_builder); + + TO_BUILDER(b)->HttpProperties().Tls(*TO_TLS_BUILDER(tls_builder)); + + LDServerHttpPropertiesTlsBuilder_Free(tls_builder); +} + +LD_EXPORT(void) +LDServerHttpPropertiesTlsBuilder_SkipVerifyPeer( + LDServerHttpPropertiesTlsBuilder b, + bool skip_verify_peer) { + LD_ASSERT_NOT_NULL(b); + + TO_TLS_BUILDER(b)->SkipVerifyPeer(skip_verify_peer); +} + +LD_EXPORT(LDServerHttpPropertiesTlsBuilder) +LDServerHttpPropertiesTlsBuilder_New(void) { + return FROM_TLS_BUILDER(new TlsBuilder()); +} + +LD_EXPORT(void) +LDServerHttpPropertiesTlsBuilder_Free(LDServerHttpPropertiesTlsBuilder b) { + delete TO_TLS_BUILDER(b); +} + LD_EXPORT(void) LDServerConfigBuilder_Logging_Disable(LDServerConfigBuilder b) { LD_ASSERT_NOT_NULL(b); diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index f88eb5cc9..0cd64e32d 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -52,11 +52,16 @@ PollingDataSource::PollingDataSource( config::built::HttpProperties const& http_properties) : logger_(logger), status_manager_(status_manager), - requester_(ioc), + requester_(ioc, http_properties.Tls().PeerVerifyMode()), polling_interval_(data_source_config.poll_interval), request_(MakeRequest(data_source_config, endpoints, http_properties)), timer_(ioc), sink_(nullptr) { + if (http_properties.Tls().PeerVerifyMode() == + launchdarkly::config::shared::built::TlsOptions::VerifyMode:: + kVerifyNone) { + LD_LOG(logger_, LogLevel::kDebug) << "TLS peer verification disabled"; + } if (polling_interval_ < data_source_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval too frequent, defaulting to " diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index 923939774..d58d7662f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -104,6 +104,12 @@ void StreamingDataSource::StartAsync( client_builder.header(key, value); } + if (http_config_.Tls().PeerVerifyMode() == + launchdarkly::config::shared::built::TlsOptions::VerifyMode:: + kVerifyNone) { + client_builder.skip_verify_peer(true); + } + auto weak_self = weak_from_this(); client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 996d6d7e1..ba5a5e3f7 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -250,3 +250,18 @@ TEST(ClientBindings, LazyLoadDataSource) { LDStatus_Free(status); } + +TEST(ClientBindings, TlsConfiguration) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerHttpPropertiesTlsBuilder tls = + LDServerHttpPropertiesTlsBuilder_New(); + LDServerHttpPropertiesTlsBuilder_SkipVerifyPeer(tls, true); + + LDServerConfigBuilder_HttpProperties_Tls(cfg_builder, tls); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + + LDServerConfig_Free(config); +} diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 9b9e191c6..1143f13b5 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -131,6 +131,15 @@ class Builder { */ Builder& errors(ErrorCallback callback); + /** + * If connecting to an endpoint with TLS, whether to skip verifying the + * remote peer's certificates. Verification is enabled by default. + * + * @param skip_verify_peer True to skip verification, false to verify. + * @return Reference to this builder. + */ + Builder& skip_verify_peer(bool skip_verify_peer); + /** * Builds a Client. The shared pointer is necessary to extend the lifetime * of the Client to encompass each asynchronous operation that it performs. @@ -150,6 +159,7 @@ class Builder { LogCallback logging_cb_; EventReceiver receiver_; ErrorCallback error_cb_; + bool skip_verify_peer_; }; /** diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index c3c051365..d9dd11afc 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -503,7 +503,8 @@ Builder::Builder(net::any_io_executor ctx, std::string url) initial_reconnect_delay_{std::nullopt}, logging_cb_([](auto msg) {}), receiver_([](launchdarkly::sse::Event const&) {}), - error_cb_([](auto err) {}) { + error_cb_([](auto err) {}), + skip_verify_peer_(false) { request_.version(11); request_.set(http::field::user_agent, kDefaultUserAgent); request_.method(http::verb::get); @@ -561,6 +562,11 @@ Builder& Builder::errors(ErrorCallback callback) { return *this; } +Builder& Builder::skip_verify_peer(bool skip_verify_peer) { + skip_verify_peer_ = skip_verify_peer; + return *this; +} + std::shared_ptr Builder::build() { auto uri_components = boost::urls::parse_uri(url_); if (!uri_components) { @@ -607,6 +613,10 @@ std::shared_ptr Builder::build() { if (uri_components->scheme_id() == boost::urls::scheme::https) { ssl = launchdarkly::foxy::make_ssl_ctx(ssl::context::tlsv12_client); ssl->set_default_verify_paths(); + if (skip_verify_peer_) { + ssl->set_verify_mode(ssl::context::verify_none); + logging_cb_("TLS peer verification disabled"); + } } return std::make_shared(