Skip to content
Merged
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
1 change: 1 addition & 0 deletions libs/internal/include/launchdarkly/async/promise.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <memory>
#include <mutex>
#include <optional>
#include <variant>
#include <vector>

namespace launchdarkly::async {
Expand Down
9 changes: 9 additions & 0 deletions libs/internal/include/launchdarkly/encoding/base_64.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ namespace launchdarkly::encoding {
*/
std::string Base64UrlEncode(std::string const& input);

/**
* Return a standard base64 encoded version of the input string, using the
* RFC 4648 section 4 alphabet (with '+' and '/'). Unlike @ref Base64UrlEncode,
* this is NOT URL-safe.
* @param input The string to Base64 encode.
* @return The encoded value.
*/
std::string Base64Encode(std::string const& input);

} // namespace launchdarkly::encoding
16 changes: 16 additions & 0 deletions libs/internal/src/encoding/base_64.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include <launchdarkly/encoding/base_64.hpp>

#include <openssl/evp.h>

#include <algorithm>
#include <array>
#include <bitset>
#include <climits>
#include <cstddef>
#include <vector>

static unsigned char const kEncodeSize = 4;
static unsigned char const kInputBytesPerEncodeSize = 3;
Expand Down Expand Up @@ -75,4 +78,17 @@ std::string Base64UrlEncode(std::string const& input) {
return out;
}

std::string Base64Encode(std::string const& input) {
if (input.empty()) {
return {};
}
// EVP_EncodeBlock writes 4 output characters per 3 input bytes (rounded up)
// plus a NUL terminator.
std::vector<unsigned char> out(4 * ((input.size() + 2) / 3) + 1);
int const written = EVP_EncodeBlock(
out.data(), reinterpret_cast<unsigned char const*>(input.data()),
static_cast<int>(input.size()));
return std::string(reinterpret_cast<char const*>(out.data()), written);
}

} // namespace launchdarkly::encoding
24 changes: 23 additions & 1 deletion libs/internal/tests/base_64_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

#include "launchdarkly/encoding/base_64.hpp"

using launchdarkly::encoding::Base64Encode;
using launchdarkly::encoding::Base64UrlEncode;

TEST(Base64Encoding, CanEncodeString) {
// Test vectors from RFC4668
// Test vectors from RFC4648
// https://datatracker.ietf.org/doc/html/rfc4648#section-10
EXPECT_EQ("", Base64UrlEncode(""));
EXPECT_EQ("Zg==", Base64UrlEncode("f"));
Expand All @@ -15,3 +16,24 @@ TEST(Base64Encoding, CanEncodeString) {
EXPECT_EQ("Zm9vYmE=", Base64UrlEncode("fooba"));
EXPECT_EQ("Zm9vYmFy", Base64UrlEncode("foobar"));
}

TEST(Base64Encoding, StandardCanEncodeString) {
// Test vectors from RFC4648
// https://datatracker.ietf.org/doc/html/rfc4648#section-10
EXPECT_EQ("", Base64Encode(""));
EXPECT_EQ("Zg==", Base64Encode("f"));
EXPECT_EQ("Zm8=", Base64Encode("fo"));
EXPECT_EQ("Zm9v", Base64Encode("foo"));
EXPECT_EQ("Zm9vYg==", Base64Encode("foob"));
EXPECT_EQ("Zm9vYmE=", Base64Encode("fooba"));
EXPECT_EQ("Zm9vYmFy", Base64Encode("foobar"));
}

TEST(Base64Encoding, StandardUsesNonUrlSafeAlphabet) {
// "???" encodes to a value ending in '/' under the standard alphabet and
// '_' under the URL-safe one; ">>>" exercises '+' versus '-'.
EXPECT_EQ("Pz8/", Base64Encode("???"));
EXPECT_EQ("Pz8_", Base64UrlEncode("???"));
EXPECT_EQ("Pj4+", Base64Encode(">>>"));
EXPECT_EQ("Pj4-", Base64UrlEncode(">>>"));
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ class DynamoDBBigSegmentStore final : public IBigSegmentStore {
DynamoDBClientOptions options = {});

[[nodiscard]] GetMembershipResult GetMembership(
std::string const& context_hash) const override;
[[nodiscard]] GetMetadataResult GetMetadata() const override;
std::string const& context_hash) const noexcept override;
[[nodiscard]] GetMetadataResult GetMetadata() const noexcept override;

~DynamoDBBigSegmentStore() override;

Expand Down
179 changes: 97 additions & 82 deletions libs/server-sdk-dynamodb-source/src/dynamodb_big_segment_store.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include <launchdarkly/server_side/integrations/dynamodb/dynamodb_big_segment_store.hpp>

#include "aws_sdk_guard.hpp"
Expand Down Expand Up @@ -64,100 +64,115 @@
DynamoDBBigSegmentStore::~DynamoDBBigSegmentStore() = default;

IBigSegmentStore::GetMembershipResult DynamoDBBigSegmentStore::GetMembership(
std::string const& context_hash) const {
Aws::DynamoDB::Model::GetItemRequest request;
request.SetTableName(table_name_);
request.SetConsistentRead(true);
request.AddKey(kPartitionKey,
Aws::DynamoDB::Model::AttributeValue{user_namespace_});
request.AddKey(kSortKey,
Aws::DynamoDB::Model::AttributeValue{context_hash});

auto outcome = client_->GetItem(request);
if (!outcome.IsSuccess()) {
return tl::make_unexpected(outcome.GetError().GetMessage());
}

auto const& item = outcome.GetResult().GetItem();

std::vector<std::string> included;
std::vector<std::string> excluded;

// GetSS() silently returns an empty vector if the attribute is not
// actually a String Set, so check the type explicitly before reading.
if (auto const it = item.find(kBigSegmentsIncludedAttribute);
it != item.end()) {
if (it->second.GetType() !=
Aws::DynamoDB::Model::ValueType::STRING_SET) {
return tl::make_unexpected(
std::string("DynamoDB Big Segments '") +
kBigSegmentsIncludedAttribute +
"' is not of type STRING_SET");
}
for (auto const& ref : it->second.GetSS()) {
included.emplace_back(ref);
std::string const& context_hash) const noexcept {
try {
Aws::DynamoDB::Model::GetItemRequest request;
request.SetTableName(table_name_);
request.SetConsistentRead(true);
request.AddKey(kPartitionKey,
Aws::DynamoDB::Model::AttributeValue{user_namespace_});
request.AddKey(kSortKey,
Aws::DynamoDB::Model::AttributeValue{context_hash});

auto outcome = client_->GetItem(request);
if (!outcome.IsSuccess()) {
return tl::make_unexpected(outcome.GetError().GetMessage());
}
}
if (auto const it = item.find(kBigSegmentsExcludedAttribute);
it != item.end()) {
if (it->second.GetType() !=
Aws::DynamoDB::Model::ValueType::STRING_SET) {
return tl::make_unexpected(
std::string("DynamoDB Big Segments '") +
kBigSegmentsExcludedAttribute +
"' is not of type STRING_SET");
Comment thread
beekld marked this conversation as resolved.

auto const& item = outcome.GetResult().GetItem();

std::vector<std::string> included;
std::vector<std::string> excluded;

// GetSS() silently returns an empty vector if the attribute is not
// actually a String Set, so check the type explicitly before reading.
if (auto const it = item.find(kBigSegmentsIncludedAttribute);
it != item.end()) {
if (it->second.GetType() !=
Aws::DynamoDB::Model::ValueType::STRING_SET) {
return tl::make_unexpected(
std::string("DynamoDB Big Segments '") +
kBigSegmentsIncludedAttribute +
"' is not of type STRING_SET");
}
for (auto const& ref : it->second.GetSS()) {
included.emplace_back(ref);
}
}
for (auto const& ref : it->second.GetSS()) {
excluded.emplace_back(ref);
if (auto const it = item.find(kBigSegmentsExcludedAttribute);
it != item.end()) {
if (it->second.GetType() !=
Aws::DynamoDB::Model::ValueType::STRING_SET) {
return tl::make_unexpected(
std::string("DynamoDB Big Segments '") +
kBigSegmentsExcludedAttribute +
"' is not of type STRING_SET");
}
for (auto const& ref : it->second.GetSS()) {
excluded.emplace_back(ref);
}
}
}

return Membership::FromSegmentRefs(included, excluded);
return Membership::FromSegmentRefs(included, excluded);
} catch (std::exception const& e) {
return tl::make_unexpected(e.what());
}
}

IBigSegmentStore::GetMetadataResult DynamoDBBigSegmentStore::GetMetadata()
const {
Aws::DynamoDB::Model::GetItemRequest request;
request.SetTableName(table_name_);
request.SetConsistentRead(true);
request.AddKey(kPartitionKey,
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});
request.AddKey(kSortKey,
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});

auto outcome = client_->GetItem(request);
if (!outcome.IsSuccess()) {
return tl::make_unexpected(outcome.GetError().GetMessage());
}
const noexcept {
try {
Aws::DynamoDB::Model::GetItemRequest request;
request.SetTableName(table_name_);
request.SetConsistentRead(true);
request.AddKey(
kPartitionKey,
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});
request.AddKey(
kSortKey,
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});

auto outcome = client_->GetItem(request);
if (!outcome.IsSuccess()) {
return tl::make_unexpected(outcome.GetError().GetMessage());
}

auto const& item = outcome.GetResult().GetItem();
if (item.empty()) {
return std::nullopt;
}
auto const& item = outcome.GetResult().GetItem();
if (item.empty()) {
return std::nullopt;
}

auto const it = item.find(kBigSegmentsSyncTimeAttribute);
if (it == item.end()) {
// "absent" sync time is treated as never synchronized rather than
// an error; the wrapper marks the store stale based on the
// resulting nullopt.
return std::nullopt;
}
auto const it = item.find(kBigSegmentsSyncTimeAttribute);
if (it == item.end()) {
// "absent" sync time is treated as never synchronized rather than
// an error; the wrapper marks the store stale based on the
// resulting nullopt.
return std::nullopt;
}

auto const& raw = it->second.GetN();
if (raw.empty()) {
return tl::make_unexpected(
"DynamoDB Big Segments 'synchronizedOn' is empty or not type N");
}
auto const& raw = it->second.GetN();
if (raw.empty()) {
return tl::make_unexpected(
"DynamoDB Big Segments 'synchronizedOn' is empty or not type "
"N");
}

errno = 0;
char* end = nullptr;
long long const parsed = std::strtoll(raw.c_str(), &end, 10);
if (errno != 0 || end == raw.c_str() || *end != '\0') {
return tl::make_unexpected(
"DynamoDB Big Segments 'synchronizedOn' is not a valid integer");
}
errno = 0;
char* end = nullptr;
long long const parsed = std::strtoll(raw.c_str(), &end, 10);
if (errno != 0 || end == raw.c_str() || *end != '\0') {
return tl::make_unexpected(
"DynamoDB Big Segments 'synchronizedOn' is not a valid "
"integer");
}

return StoreMetadata{std::chrono::milliseconds{parsed}};
// The stored value is a Unix-epoch millisecond count: system_clock's
// epoch.
return StoreMetadata{std::chrono::system_clock::time_point{
std::chrono::milliseconds{parsed}}};
} catch (std::exception const& e) {
return tl::make_unexpected(e.what());
}
}

} // namespace launchdarkly::server_side::integrations
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ TEST_F(DynamoDBBigSegmentTests, GetMetadataWithEmptyPrefix) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->has_value());
ASSERT_EQ(result->value().last_up_to_date,
std::chrono::milliseconds{1700000000000LL});
std::chrono::system_clock::time_point{
std::chrono::milliseconds{1700000000000LL}});
}

TEST_F(DynamoDBBigSegmentTests, GetMembershipRejectsMalformedIncluded) {
Expand All @@ -182,7 +183,8 @@ TEST_F(DynamoDBBigSegmentTests, GetMetadataReturnsSyncTime) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->has_value());
ASSERT_EQ(result->value().last_up_to_date,
std::chrono::milliseconds{1700000000000LL});
std::chrono::system_clock::time_point{
std::chrono::milliseconds{1700000000000LL}});
}

TEST_F(DynamoDBBigSegmentTests, GetMetadataAbsentSyncTimeReturnsNoMetadata) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ class RedisBigSegmentStore final : public IBigSegmentStore {
Create(std::string uri, std::string prefix);

[[nodiscard]] GetMembershipResult GetMembership(
std::string const& context_hash) const override;
[[nodiscard]] GetMetadataResult GetMetadata() const override;
std::string const& context_hash) const noexcept override;
[[nodiscard]] GetMetadataResult GetMetadata() const noexcept override;

~RedisBigSegmentStore() override;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ RedisBigSegmentStore::RedisBigSegmentStore(
RedisBigSegmentStore::~RedisBigSegmentStore() = default;

IBigSegmentStore::GetMembershipResult RedisBigSegmentStore::GetMembership(
std::string const& context_hash) const {
std::string const& context_hash) const noexcept {
std::string const include_key = include_key_prefix_ + context_hash;
std::string const exclude_key = exclude_key_prefix_ + context_hash;

Expand All @@ -76,7 +76,8 @@ IBigSegmentStore::GetMembershipResult RedisBigSegmentStore::GetMembership(
return Membership::FromSegmentRefs(included, excluded);
}

IBigSegmentStore::GetMetadataResult RedisBigSegmentStore::GetMetadata() const {
IBigSegmentStore::GetMetadataResult RedisBigSegmentStore::GetMetadata()
const noexcept {
sw::redis::OptionalString raw;
try {
raw = redis_->get(sync_time_key_);
Expand All @@ -98,7 +99,9 @@ IBigSegmentStore::GetMetadataResult RedisBigSegmentStore::GetMetadata() const {
"Redis Big Segments synchronized_on is not a valid integer");
}

return StoreMetadata{std::chrono::milliseconds{parsed}};
// The stored value is a Unix-epoch millisecond count: system_clock's epoch.
return StoreMetadata{std::chrono::system_clock::time_point{
std::chrono::milliseconds{parsed}}};
}

} // namespace launchdarkly::server_side::integrations
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ TEST_F(RedisBigSegmentTests, GetMetadataWithEmptyPrefix) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->has_value());
ASSERT_EQ(result->value().last_up_to_date,
std::chrono::milliseconds{1700000000000LL});
std::chrono::system_clock::time_point{
std::chrono::milliseconds{1700000000000LL}});
}

TEST_F(RedisBigSegmentTests, GetMetadataReturnsSyncTime) {
Expand All @@ -153,7 +154,8 @@ TEST_F(RedisBigSegmentTests, GetMetadataReturnsSyncTime) {
ASSERT_TRUE(result);
ASSERT_TRUE(result->has_value());
ASSERT_EQ(result->value().last_up_to_date,
std::chrono::milliseconds{1700000000000LL});
std::chrono::system_clock::time_point{
std::chrono::milliseconds{1700000000000LL}});
}

TEST_F(RedisBigSegmentTests, GetMetadataRejectsMalformedSyncTime) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ class Membership {
*/
struct StoreMetadata {
/**
* @brief Wall-clock timestamp (Unix epoch) at which the data populator
* (e.g. the LaunchDarkly Relay Proxy) last confirmed it had pushed all
* pending Big Segments updates to the store.
* @brief Wall-clock instant at which the data populator (e.g. the
* LaunchDarkly Relay Proxy) last confirmed it had pushed all pending Big
* Segments updates to the store.
*/
std::chrono::milliseconds last_up_to_date;
std::chrono::system_clock::time_point last_up_to_date;
};

} // namespace launchdarkly::server_side::integrations
Loading
Loading