Skip to content

Commit ebe3a41

Browse files
committed
- Updated HttpClientInterface to support common authentication logic.
- Updated Http implemenations for authentication flow. - Adding extracting credentials from connection config file. -
1 parent de88d30 commit ebe3a41

22 files changed

Lines changed: 765 additions & 204 deletions

CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ target_link_libraries(dbps_server_lib PUBLIC OpenSSL::SSL OpenSSL::Crypto)
160160
# Client components library (depends on httplib, nlohmann/json, and cppcodec)
161161
add_library(dbps_client_lib STATIC
162162
src/client/dbps_api_client.cpp
163+
src/client/http_client_interface.cpp
163164
src/client/httplib_client.cpp
164165
src/client/httplib_pool_registry.cpp
165166
src/client/httplib_pooled_client.cpp
@@ -364,6 +365,14 @@ if(BUILD_TESTS)
364365
gtest_main
365366
)
366367

368+
# Http client interface tests
369+
add_executable(http_client_interface_test src/client/http_client_interface_test.cpp)
370+
target_link_libraries(http_client_interface_test
371+
dbps_client_lib
372+
dbps_common_lib
373+
gtest_main
374+
)
375+
367376
# Remote DBPA tests
368377
add_executable(dbpa_remote_test src/common/dbpa_remote_test.cpp)
369378
target_link_libraries(dbpa_remote_test
@@ -393,6 +402,7 @@ if(BUILD_SHARED_LIBS)
393402
src/common/dbps_remote_shared_lib_wrapper.cpp
394403
src/common/dbpa_remote.cpp
395404
src/client/dbps_api_client.cpp
405+
src/client/http_client_interface.cpp
396406
src/client/httplib_client.cpp
397407
src/common/json_request.cpp
398408
)
@@ -496,6 +506,7 @@ if(BUILD_TESTS)
496506
dbpa_local_test
497507
httplib_pool_registry_test
498508
httplib_pooled_client_test
509+
http_client_interface_test
499510
COMMENT "Building all tests"
500511
)
501512

@@ -517,6 +528,7 @@ if(BUILD_TESTS)
517528
gtest_discover_tests(dbpa_local_test)
518529
gtest_discover_tests(httplib_pool_registry_test)
519530
gtest_discover_tests(httplib_pooled_client_test)
531+
gtest_discover_tests(http_client_interface_test)
520532
endif()
521533

522534
# All target (everything)

src/client/dbps_api_client.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ DBPSApiClient::DBPSApiClient(std::shared_ptr<HttpClientInterface> http_client)
164164
}
165165

166166
std::string DBPSApiClient::HealthCheck() {
167-
auto response = http_client_->Get("/healthz");
167+
auto response = http_client_->Get("/healthz", false);
168168

169169
if (!response.error_message.empty()) {
170170
return "Error: " + response.error_message;

src/client/dbps_api_client_test.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ bool CompareJsonStrings(const std::string& json1, const std::string& json2, cons
5858
// Mock HTTP client for testing
5959
class MockHttpClient : public HttpClientInterface {
6060
public:
61+
MockHttpClient()
62+
: HttpClientInterface(
63+
"mock://",
64+
ClientCredentials{{"client_id", "test_client_AAAA"}, {"api_key", "test_key_AAAA"}}) {
65+
}
66+
6167
// Mock responses for different endpoints
6268
void SetMockResponse(const std::string& endpoint, const HttpResponse& response) {
6369
mock_responses_[endpoint] = response;
@@ -67,16 +73,21 @@ class MockHttpClient : public HttpClientInterface {
6773
mock_post_responses_[endpoint] = {expected_body, response};
6874
}
6975

70-
// Implement the interface methods
71-
HttpResponse Get(const std::string& endpoint) override {
76+
protected:
77+
HttpResponse DoGet(const std::string& endpoint, const HeaderList& headers) override {
78+
(void)headers;
7279
auto it = mock_responses_.find(endpoint);
7380
if (it != mock_responses_.end()) {
7481
return it->second;
7582
}
7683
return HttpResponse(404, "", "Mock endpoint not found: " + endpoint);
7784
}
7885

79-
HttpResponse Post(const std::string& endpoint, const std::string& json_body) override {
86+
HttpResponse DoPost(const std::string& endpoint, const std::string& json_body, const HeaderList& headers) override {
87+
(void)headers;
88+
if (endpoint == "/token") {
89+
return HttpResponse(200, R"({"token":"mock_jwt","token_type":"Bearer","expires_at":1766138275})");
90+
}
8091
auto it = mock_post_responses_.find(endpoint);
8192
if (it != mock_post_responses_.end()) {
8293
if (CompareJsonStrings(it->second.first, json_body, {"debug"})) {
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#include "http_client_interface.h"
19+
20+
#include <chrono>
21+
22+
#include "json_request.h"
23+
24+
HttpClientInterface::HeaderList HttpClientInterface::DefaultJsonGetHeaders() {
25+
HeaderList headers;
26+
headers.insert({"Accept", kJsonContentType});
27+
headers.insert({"User-Agent", kDefaultUserAgent});
28+
return headers;
29+
}
30+
31+
HttpClientInterface::HeaderList HttpClientInterface::DefaultJsonPostHeaders() {
32+
HeaderList headers;
33+
headers.insert({"Content-Type", kJsonContentType});
34+
headers.insert({"Accept", kJsonContentType});
35+
headers.insert({"User-Agent", kDefaultUserAgent});
36+
return headers;
37+
}
38+
39+
HttpClientInterface::HttpResponse HttpClientInterface::Get(const std::string& endpoint, bool auth_required) {
40+
const auto attempt = [&]() -> HttpResponse {
41+
auto headers = DefaultJsonGetHeaders();
42+
if (auth_required) {
43+
auto auth_error = AddAuthorizationHeader(headers);
44+
if (!auth_error.empty()) {
45+
return HttpResponse(0, "", auth_error);
46+
}
47+
}
48+
return DoGet(endpoint, headers);
49+
};
50+
51+
auto result = attempt();
52+
if (auth_required && result.status_code == 401) {
53+
InvalidateCachedToken();
54+
result = attempt();
55+
}
56+
return result;
57+
}
58+
59+
HttpClientInterface::HttpResponse HttpClientInterface::Post(const std::string& endpoint,
60+
const std::string& json_body,
61+
bool auth_required) {
62+
const auto attempt = [&]() -> HttpResponse {
63+
auto headers = DefaultJsonPostHeaders();
64+
if (auth_required) {
65+
auto auth_error = AddAuthorizationHeader(headers);
66+
if (!auth_error.empty()) {
67+
return HttpResponse(0, "", auth_error);
68+
}
69+
}
70+
return DoPost(endpoint, json_body, headers);
71+
};
72+
73+
auto result = attempt();
74+
if (auth_required && result.status_code == 401) {
75+
InvalidateCachedToken();
76+
result = attempt();
77+
}
78+
return result;
79+
}
80+
81+
std::string HttpClientInterface::AddAuthorizationHeader(HeaderList& headers) {
82+
std::string token_or_error;
83+
auto token_opt = EnsureValidToken(token_or_error);
84+
if (!token_opt.has_value()) {
85+
return token_or_error;
86+
}
87+
88+
headers.erase(kAuthorizationHeader);
89+
std::string auth_value = token_opt->token_type;
90+
if (auth_value.back() != ' ') {
91+
auth_value.push_back(' ');
92+
}
93+
auth_value += token_opt->token;
94+
headers.insert({kAuthorizationHeader, auth_value});
95+
return "";
96+
}
97+
98+
std::optional<HttpClientInterface::CachedToken> HttpClientInterface::EnsureValidToken(std::string& error) {
99+
100+
const auto now_epoch_seconds = []() -> std::int64_t {
101+
const auto now = std::chrono::system_clock::now();
102+
const auto secs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
103+
return static_cast<std::int64_t>(secs);
104+
};
105+
106+
const auto is_token_valid_at = [&](std::int64_t now) -> bool {
107+
return cached_token_.has_value() &&
108+
!cached_token_->token.empty() &&
109+
cached_token_->expires_at > (now + kTokenExpirySkewSeconds);
110+
};
111+
112+
error.clear();
113+
const auto now = now_epoch_seconds();
114+
115+
{
116+
std::unique_lock<std::mutex> lock(token_mutex_);
117+
if (is_token_valid_at(now)) {
118+
return cached_token_;
119+
}
120+
while (token_fetch_in_progress_) {
121+
token_cv_.wait(lock);
122+
if (is_token_valid_at(now_epoch_seconds())) {
123+
return cached_token_;
124+
}
125+
}
126+
token_fetch_in_progress_ = true;
127+
}
128+
129+
// Fetch token without holding token_mutex_.
130+
std::optional<CachedToken> fetched = FetchToken(error);
131+
132+
{
133+
std::lock_guard<std::mutex> lock(token_mutex_);
134+
token_fetch_in_progress_ = false;
135+
if (fetched.has_value()) {
136+
cached_token_ = fetched;
137+
}
138+
}
139+
token_cv_.notify_all();
140+
return fetched;
141+
}
142+
143+
std::optional<HttpClientInterface::CachedToken> HttpClientInterface::FetchToken(std::string& error) {
144+
error.clear();
145+
TokenRequest token_req;
146+
token_req.credential_values_ = credentials_;
147+
148+
// IMPORTANT: call DoPost directly (authless) to avoid recursion.
149+
auto http_resp = DoPost("/token", token_req.ToJson(), DefaultJsonPostHeaders());
150+
if (!http_resp.error_message.empty() || http_resp.status_code != 200) {
151+
error = http_resp.error_message + " (status code: " + std::to_string(http_resp.status_code) + ")";
152+
return std::nullopt;
153+
}
154+
155+
TokenResponse token_resp;
156+
token_resp.Parse(http_resp.result);
157+
if (!token_resp.IsValid()) {
158+
error = token_resp.GetValidationError();
159+
if (error.empty()) {
160+
error = "While reading token response, found an invalid token response: " + http_resp.result;
161+
}
162+
return std::nullopt;
163+
}
164+
165+
CachedToken result;
166+
result.token = token_resp.token_.value();
167+
result.token_type = token_resp.token_type_.value();
168+
result.expires_at = token_resp.expires_at_.value();
169+
return result;
170+
}
171+
172+
void HttpClientInterface::InvalidateCachedToken() {
173+
std::lock_guard<std::mutex> lock(token_mutex_);
174+
cached_token_ = std::nullopt;
175+
}

src/client/http_client_interface.h

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
#pragma once
1919

20+
#include <condition_variable>
21+
#include <cstdint>
22+
#include <map>
23+
#include <mutex>
24+
#include <optional>
2025
#include <string>
2126
#include <httplib.h>
2227

@@ -30,10 +35,13 @@ class HttpClientInterface {
3035
public:
3136
virtual ~HttpClientInterface() = default;
3237

38+
using ClientCredentials = std::map<std::string, std::string>;
3339
using HeaderList = httplib::Headers;
3440

3541
static constexpr const char* kJsonContentType = "application/json";
3642
static constexpr const char* kDefaultUserAgent = "DBPSApiClient/1.0";
43+
static constexpr const char* kAuthorizationHeader = "Authorization";
44+
static inline constexpr std::int64_t kTokenExpirySkewSeconds = 30;
3745

3846
struct HttpResponse {
3947
int status_code;
@@ -49,21 +57,43 @@ class HttpClientInterface {
4957
: status_code(code), result(std::move(response_result)), error_message(std::move(error)) {}
5058
};
5159

52-
static HeaderList DefaultJsonGetHeaders() {
53-
HeaderList headers;
54-
headers.insert({"Accept", kJsonContentType});
55-
headers.insert({"User-Agent", kDefaultUserAgent});
56-
return headers;
57-
}
60+
HttpResponse Get(const std::string& endpoint, bool auth_required = true);
61+
HttpResponse Post(const std::string& endpoint, const std::string& json_body, bool auth_required = true);
5862

59-
static HeaderList DefaultJsonPostHeaders() {
60-
HeaderList headers;
61-
headers.insert({"Content-Type", kJsonContentType});
62-
headers.insert({"Accept", kJsonContentType});
63-
headers.insert({"User-Agent", kDefaultUserAgent});
64-
return headers;
63+
protected:
64+
explicit HttpClientInterface(std::string base_url,
65+
ClientCredentials credentials = {})
66+
: base_url_(std::move(base_url)),
67+
credentials_(std::move(credentials)) {
6568
}
6669

67-
virtual HttpResponse Get(const std::string& endpoint) = 0;
68-
virtual HttpResponse Post(const std::string& endpoint, const std::string& json_body) = 0;
70+
virtual HttpResponse DoGet(const std::string& endpoint, const HeaderList& headers) = 0;
71+
virtual HttpResponse DoPost(const std::string& endpoint, const std::string& json_body, const HeaderList& headers) = 0;
72+
73+
const std::string base_url_;
74+
const ClientCredentials credentials_;
75+
76+
private:
77+
// Header list helpers
78+
static HeaderList DefaultJsonGetHeaders();
79+
static HeaderList DefaultJsonPostHeaders();
80+
std::string AddAuthorizationHeader(HeaderList& headers);
81+
82+
// Token variable
83+
struct CachedToken {
84+
std::string token;
85+
std::string token_type;
86+
std::int64_t expires_at = 0;
87+
};
88+
std::optional<CachedToken> cached_token_;
89+
90+
// Thread-safe synchronization variables while fetching token
91+
std::mutex token_mutex_;
92+
std::condition_variable token_cv_;
93+
bool token_fetch_in_progress_ = false;
94+
95+
// Thread-safe synchronization functions while fetching token
96+
std::optional<CachedToken> EnsureValidToken(std::string& error);
97+
std::optional<CachedToken> FetchToken(std::string& error);
98+
void InvalidateCachedToken();
6999
};

0 commit comments

Comments
 (0)