4646#include < regex>
4747#include < cstdio>
4848#include < string>
49+ #include < string_view>
50+ #include < utility>
4951#include < TAlienUserAgent.h>
5052#include < unordered_set>
5153#include " rapidjson/document.h"
@@ -62,24 +64,74 @@ unique_ptr<TJAlienCredentials> CcdbApi::mJAlienCredentials = nullptr;
6264
6365namespace
6466{
65- // / Append the gate token, if ALICEO2_CCDB_AUTH_TOKEN names one, to a header list .
67+ // / Strip surrounding whitespace, CR and LF included .
6668// /
67- // / Set when CCDB is reached through a broker that authenticates its callers.
68- // / The CI does this so the credential CCDB wants for writes -- a grid
69- // / certificate -- stays in the broker and never enters the build container,
70- // / which runs pull-request code. The broker consumes this header and does not
71- // / forward it, so CCDB itself never sees it.
72- // /
73- // / Read once into a static: getenv races setenv, and these paths run from
74- // / several threads. Returns the list unchanged when no token is configured, so
75- // / callers can apply it unconditionally.
76- curl_slist* appendGateToken (curl_slist* list)
77- {
78- static const std::string header = []() -> std::string {
79- const char * token = getenv (" ALICEO2_CCDB_AUTH_TOKEN" );
80- return (token && *token) ? std::string (" Authorization: Bearer " ) + token : std::string ();
69+ // / A value that keeps its line's trailing CRLF ends the header block early when
70+ // / it is spliced back into a request, silently dropping every header after it.
71+ std::string_view trimHeaderValue (std::string_view value)
72+ {
73+ constexpr std::string_view whitespace = " \t\r\n " ;
74+ const auto first = value.find_first_not_of (whitespace);
75+ return first == std::string_view::npos
76+ ? std::string_view{}
77+ : value.substr (first, value.find_last_not_of (whitespace) - first + 1 );
78+ }
79+
80+ // / Gate tokens per endpoint, "<url>=<token>;<url>=<token>", from
81+ // / ALICEO2_CCDB_AUTH_TOKENS. Set when CCDB sits behind a broker that
82+ // / authenticates its callers: the broker mints tokens per route, so a process
83+ // / facing two CCDBs (writable test instance, production) carries one per
84+ // / endpoint. Longest prefix first; read once into a static, since getenv races
85+ // / setenv and these paths run from several threads.
86+ const std::vector<std::pair<std::string, std::string>>& gateTokenTable ()
87+ {
88+ static const auto table = []() {
89+ std::vector<std::pair<std::string, std::string>> entries;
90+ const char * spec = getenv (" ALICEO2_CCDB_AUTH_TOKENS" );
91+ std::string_view rest = spec ? spec : " " ;
92+ while (!rest.empty ()) {
93+ const auto sep = rest.find (' ;' );
94+ const auto entry = trimHeaderValue (rest.substr (0 , sep));
95+ rest = (sep == std::string_view::npos) ? std::string_view{} : rest.substr (sep + 1 );
96+ const auto eq = entry.find (' =' );
97+ if (eq == std::string_view::npos) {
98+ continue ;
99+ }
100+ auto url = trimHeaderValue (entry.substr (0 , eq));
101+ // Trimmed: a stray newline in a token makes the request malformed, which
102+ // a strict broker rejects with an opaque 400 rather than an auth error.
103+ const auto token = trimHeaderValue (entry.substr (eq + 1 ));
104+ while (url.size () > 1 && url.back () == ' /' ) { // normalise, so the boundary test below is exact
105+ url.remove_suffix (1 );
106+ }
107+ if (!url.empty () && !token.empty ()) {
108+ entries.emplace_back (std::string (url), std::string (" Authorization: Bearer " ).append (token));
109+ }
110+ }
111+ std::sort (entries.begin (), entries.end (),
112+ [](const auto & a, const auto & b) { return a.first .size () > b.first .size (); });
113+ return entries;
81114 }();
82- return header.empty () ? list : curl_slist_append (list, header.c_str ());
115+ return table;
116+ }
117+
118+ // / Append the gate token for `url`, if any, to a header list.
119+ // /
120+ // / The URL decides the token, so a multi-host pool (initHostsPool splits on
121+ // / ',') gets the right one per host -- which also means the list must be built
122+ // / per host, never shared across a pool. Matching stops at a path boundary:
123+ // / ".../ccdb" is a prefix of ".../ccdb-prod", and a bare startswith would hand
124+ // / production the test instance's token whenever the production entry is
125+ // / missing -- an opaque 401. No match, no token.
126+ curl_slist* appendGateToken (curl_slist* list, std::string_view url)
127+ {
128+ for (const auto & [prefix, header] : gateTokenTable ()) {
129+ if (url.substr (0 , prefix.size ()) == prefix &&
130+ (url.size () == prefix.size () || url[prefix.size ()] == ' /' )) {
131+ return curl_slist_append (list, header.c_str ());
132+ }
133+ }
134+ return list;
83135}
84136} // namespace
85137
@@ -447,16 +499,9 @@ int CcdbApi::storeAsBinaryFile(const char* buffer, size_t size, const std::strin
447499 curl_mime_data (field, " " , 0 );
448500 }
449501
450- struct curl_slist * headerlist = nullptr ;
451- static const char buf[] = " Expect:" ;
452- headerlist = curl_slist_append (headerlist, buf);
453-
454- headerlist = appendGateToken (headerlist);
455-
456502 curlSetSSLOptions (curl);
457503
458504 curl_easy_setopt (curl, CURLOPT_MIMEPOST , mime);
459- curl_easy_setopt (curl, CURLOPT_HTTPHEADER , headerlist);
460505 curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION , 1L );
461506 curl_easy_setopt (curl, CURLOPT_USERAGENT , mUniqueAgentID .c_str ());
462507 curl_easy_setopt (curl, CURLOPT_TIMEOUT , mCurlTimeoutUpload );
@@ -469,6 +514,11 @@ int CcdbApi::storeAsBinaryFile(const char* buffer, size_t size, const std::strin
469514 /* what URL that receives this POST */
470515 curl_easy_setopt (curl, CURLOPT_URL , fullUrl.c_str ());
471516
517+ // Per host: the gate token is per endpoint (see appendGateToken).
518+ struct curl_slist * headerlist = curl_slist_append (nullptr , " Expect:" );
519+ headerlist = appendGateToken (headerlist, fullUrl);
520+ curl_easy_setopt (curl, CURLOPT_HTTPHEADER , headerlist);
521+
472522 /* Perform the request, res will get the return code */
473523 res = CURL_perform (curl);
474524 /* Check for errors */
@@ -480,13 +530,12 @@ int CcdbApi::storeAsBinaryFile(const char* buffer, size_t size, const std::strin
480530 }
481531 returnValue = res;
482532 }
533+ curl_slist_free_all (headerlist);
483534 }
484535
485536 /* always cleanup */
486537 curl_easy_cleanup (curl);
487538
488- /* free slist */
489- curl_slist_free_all (headerlist);
490539 /* free mime */
491540 curl_mime_free (mime);
492541 } else {
@@ -726,7 +775,7 @@ size_t header_map_callback(char* buffer, size_t size, size_t nitems, void* userd
726775} // namespace
727776
728777void CcdbApi::initCurlHTTPHeaderOptionsForRetrieve (CURL * curlHandle, curl_slist*& option_list, long timestamp, std::map<std::string, std::string>* headers, std::string const & etag,
729- const std::string& createdNotAfter, const std::string& createdNotBefore) const
778+ const std::string& createdNotAfter, const std::string& createdNotBefore, std::string_view url ) const
730779{
731780 // struct curl_slist* list = nullptr;
732781 if (!etag.empty ()) {
@@ -747,11 +796,11 @@ void CcdbApi::initCurlHTTPHeaderOptionsForRetrieve(CURL* curlHandle, curl_slist*
747796 curl_easy_setopt (curlHandle, CURLOPT_HEADERDATA , headers);
748797 }
749798
750- option_list = appendGateToken (option_list);
799+ option_list = appendGateToken (option_list, url );
751800
752- if (option_list) {
753- curl_easy_setopt (curlHandle, CURLOPT_HTTPHEADER , option_list);
754- }
801+ // Unconditionally, nullptr included: the handle is reused across hosts, and
802+ // skipping the set would leave a previous host's freed list installed.
803+ curl_easy_setopt (curlHandle, CURLOPT_HTTPHEADER , option_list);
755804
756805 curl_easy_setopt (curlHandle, CURLOPT_USERAGENT , mUniqueAgentID .c_str ());
757806}
@@ -783,16 +832,17 @@ bool CcdbApi::receiveObject(void* dataHolder, std::string const& path, std::map<
783832
784833 curlSetSSLOptions (curlHandle);
785834 initCurlOptionsForRetrieve (curlHandle, dataHolder, writeCallback, followRedirect);
786- curl_slist* option_list = nullptr ;
787- initCurlHTTPHeaderOptionsForRetrieve (curlHandle, option_list, timestamp, headers, etag, createdNotAfter, createdNotBefore);
788-
789835 long responseCode = 0 ;
790836 CURLcode curlResultCode = CURL_LAST ;
791837
792838 for (size_t hostIndex = 0 ; hostIndex < hostsPool.size () && (responseCode >= 400 || curlResultCode > 0 ); hostIndex++) {
793839 std::string fullUrl = getFullUrlForRetrieval (curlHandle, path, metadata, timestamp, hostIndex);
794840 curl_easy_setopt (curlHandle, CURLOPT_URL , fullUrl.c_str ());
795841
842+ // Per host: the gate token is per endpoint (see appendGateToken).
843+ curl_slist* option_list = nullptr ;
844+ initCurlHTTPHeaderOptionsForRetrieve (curlHandle, option_list, timestamp, headers, etag, createdNotAfter, createdNotBefore, fullUrl);
845+
796846 curlResultCode = CURL_perform (curlHandle);
797847
798848 if (curlResultCode != CURLE_OK ) {
@@ -811,9 +861,9 @@ bool CcdbApi::receiveObject(void* dataHolder, std::string const& path, std::map<
811861 }
812862 }
813863 }
864+ curl_slist_free_all (option_list);
814865 }
815866
816- curl_slist_free_all (option_list);
817867 curl_easy_cleanup (curlHandle);
818868 }
819869 return false ;
@@ -1213,11 +1263,15 @@ void* CcdbApi::retrieveFromTFile(std::type_info const& tinfo, std::string const&
12131263 }
12141264
12151265 curl_slist* option_list = nullptr ;
1216- initCurlHTTPHeaderOptionsForRetrieve (curl_handle, option_list, timestamp, headers, etag, createdNotAfter, createdNotBefore);
1266+ initCurlHTTPHeaderOptionsForRetrieve (curl_handle, option_list, timestamp, headers, etag, createdNotAfter, createdNotBefore, fullUrl );
12171267 auto content = navigateURLsAndRetrieveContent (curl_handle, fullUrl, tinfo, headers);
12181268
12191269 for (size_t hostIndex = 1 ; hostIndex < hostsPool.size () && !(content); hostIndex++) {
12201270 fullUrl = getFullUrlForRetrieval (curl_handle, path, metadata, timestamp, hostIndex);
1271+ // Per host: the gate token is per endpoint (see appendGateToken).
1272+ curl_slist_free_all (option_list);
1273+ option_list = nullptr ;
1274+ initCurlHTTPHeaderOptionsForRetrieve (curl_handle, option_list, timestamp, headers, etag, createdNotAfter, createdNotBefore, fullUrl);
12211275 content = navigateURLsAndRetrieveContent (curl_handle, fullUrl, tinfo, headers);
12221276 }
12231277 if (content) {
@@ -1255,18 +1309,6 @@ std::string CcdbApi::list(std::string const& path, bool latestOnly, std::string
12551309 curl_easy_setopt (curl, CURLOPT_WRITEDATA , &result);
12561310 curl_easy_setopt (curl, CURLOPT_USERAGENT , mUniqueAgentID .c_str ());
12571311
1258- struct curl_slist * headers = nullptr ;
1259- headers = curl_slist_append (headers, (std::string (" Accept: " ) + returnFormat).c_str ());
1260- headers = curl_slist_append (headers, (std::string (" Content-Type: " ) + returnFormat).c_str ());
1261- if (createdNotAfter >= 0 ) {
1262- headers = curl_slist_append (headers, (" If-Not-After: " + std::to_string (createdNotAfter)).c_str ());
1263- }
1264- if (createdNotBefore >= 0 ) {
1265- headers = curl_slist_append (headers, (" If-Not-Before: " + std::to_string (createdNotBefore)).c_str ());
1266- }
1267- headers = appendGateToken (headers);
1268- curl_easy_setopt (curl, CURLOPT_HTTPHEADER , headers);
1269-
12701312 curlSetSSLOptions (curl);
12711313
12721314 std::string fullUrl;
@@ -1277,12 +1319,25 @@ std::string CcdbApi::list(std::string const& path, bool latestOnly, std::string
12771319 fullUrl += path;
12781320 curl_easy_setopt (curl, CURLOPT_URL , fullUrl.c_str ());
12791321
1322+ // Per host: the gate token is per endpoint (see appendGateToken).
1323+ struct curl_slist * headers = nullptr ;
1324+ headers = curl_slist_append (headers, (std::string (" Accept: " ) + returnFormat).c_str ());
1325+ headers = curl_slist_append (headers, (std::string (" Content-Type: " ) + returnFormat).c_str ());
1326+ if (createdNotAfter >= 0 ) {
1327+ headers = curl_slist_append (headers, (" If-Not-After: " + std::to_string (createdNotAfter)).c_str ());
1328+ }
1329+ if (createdNotBefore >= 0 ) {
1330+ headers = curl_slist_append (headers, (" If-Not-Before: " + std::to_string (createdNotBefore)).c_str ());
1331+ }
1332+ headers = appendGateToken (headers, fullUrl);
1333+ curl_easy_setopt (curl, CURLOPT_HTTPHEADER , headers);
1334+
12801335 res = CURL_perform (curl);
12811336 if (res != CURLE_OK ) {
12821337 LOGP (alarm, " CURL_perform() failed: {}" , curl_easy_strerror (res));
12831338 }
1339+ curl_slist_free_all (headers);
12841340 }
1285- curl_slist_free_all (headers);
12861341 curl_easy_cleanup (curl);
12871342 }
12881343
@@ -1319,16 +1374,21 @@ void CcdbApi::deleteObject(std::string const& path, long timestamp) const
13191374 fullUrl << getHostUrl (hostIndex) << " /" << path << " /" << timestampLocal;
13201375 curl_easy_setopt (curl, CURLOPT_URL , fullUrl.str ().c_str ());
13211376
1377+ // A DELETE is a write, so it needs the gate token as storing does -- per
1378+ // host, since the token is per endpoint (see appendGateToken).
1379+ struct curl_slist * list = appendGateToken (nullptr , fullUrl.str ());
1380+ curl_easy_setopt (curl, CURLOPT_HTTPHEADER , list);
1381+
13221382 // Perform the request, res will get the return code
13231383 res = CURL_perform (curl);
13241384 if (res != CURLE_OK ) {
13251385 LOGP (alarm, " CURL_perform() failed: {}" , curl_easy_strerror (res));
13261386 }
1387+ curl_slist_free_all (list);
13271388 }
13281389 // After the loop, not inside it: cleaning up per host left every later
13291390 // iteration using a freed handle.
13301391 curl_easy_cleanup (curl);
1331- curl_slist_free_all (list);
13321392 }
13331393}
13341394
@@ -1353,7 +1413,7 @@ void CcdbApi::truncate(std::string const& path) const
13531413 // does. This was the one write path left without it, which a broker
13541414 // answers 401 -- failing every CCDB suite in their teardown, since each
13551415 // one truncates the path it just wrote.
1356- struct curl_slist * list = appendGateToken (nullptr );
1416+ struct curl_slist * list = appendGateToken (nullptr , fullUrl. str () );
13571417 curl_easy_setopt (curl, CURLOPT_HTTPHEADER , list);
13581418 curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION , 1L );
13591419 curlSetSSLOptions (curl);
@@ -1512,7 +1572,7 @@ std::map<std::string, std::string> CcdbApi::retrieveHeaders(std::string const& p
15121572 if (curl != nullptr ) {
15131573 struct curl_slist * list = nullptr ;
15141574 list = curl_slist_append (list, (" If-None-Match: " + std::to_string (timestamp)).c_str ());
1515- list = appendGateToken (list);
1575+ list = appendGateToken (list, fullUrl );
15161576
15171577 curl_easy_setopt (curl, CURLOPT_HTTPHEADER , list);
15181578
@@ -1591,7 +1651,7 @@ bool CcdbApi::getCCDBEntryHeaders(std::string const& url, std::string const& eta
15911651
15921652 struct curl_slist * list = nullptr ;
15931653 list = curl_slist_append (list, (" If-None-Match: " + etag).c_str ());
1594- list = appendGateToken (list);
1654+ list = appendGateToken (list, url );
15951655
15961656 curl_easy_setopt (curl, CURLOPT_HTTPHEADER , list);
15971657
@@ -1621,11 +1681,13 @@ void CcdbApi::parseCCDBHeaders(std::vector<std::string> const& headers, std::vec
16211681{
16221682 static std::string etagHeader = " ETag: " ;
16231683 static std::string locationHeader = " Content-Location: " ;
1684+ // Trimmed: `headers` holds raw header lines, CRLF and all, and the etag goes
1685+ // straight back out as an If-None-Match request header.
16241686 for (auto h : headers) {
16251687 if (h.find (etagHeader) == 0 ) {
1626- etag = std::string (h. data () + etagHeader.size ());
1688+ etag = trimHeaderValue ( std::string_view (h). substr ( etagHeader.size () ));
16271689 } else if (h.find (locationHeader) == 0 ) {
1628- pfns.emplace_back (std::string (h. data () + locationHeader. size (), h. size () - locationHeader.size ()));
1690+ pfns.emplace_back (trimHeaderValue ( std::string_view (h). substr ( locationHeader.size () )));
16291691 }
16301692 }
16311693}
@@ -1692,8 +1754,6 @@ int CcdbApi::updateMetadata(std::string const& path, std::map<std::string, std::
16921754 curl_easy_setopt (curl, CURLOPT_USERAGENT , mUniqueAgentID .c_str ());
16931755 if (curl != nullptr ) {
16941756 CURLcode res;
1695- // A PUT is a write, so it needs the gate token as storing does.
1696- struct curl_slist * list = appendGateToken (nullptr );
16971757 for (size_t hostIndex = 0 ; hostIndex < hostsPool.size (); hostIndex++) {
16981758 // Inside the loop: hoisted out, the stream accumulates and the second
16991759 // host's URL is the first with the second appended.
@@ -1724,6 +1784,9 @@ int CcdbApi::updateMetadata(std::string const& path, std::map<std::string, std::
17241784 curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST , " PUT" ); // make sure we use PUT
17251785 curl_easy_setopt (curl, CURLOPT_USERAGENT , mUniqueAgentID .c_str ());
17261786 curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION , 1L );
1787+ // A PUT is a write, so it needs the gate token as storing does -- per
1788+ // host, since the token is per endpoint (see appendGateToken).
1789+ struct curl_slist * list = appendGateToken (nullptr , fullUrl.str ());
17271790 curl_easy_setopt (curl, CURLOPT_HTTPHEADER , list);
17281791 curlSetSSLOptions (curl);
17291792
@@ -1735,12 +1798,12 @@ int CcdbApi::updateMetadata(std::string const& path, std::map<std::string, std::
17351798 } else {
17361799 ret = 0 ;
17371800 }
1801+ curl_slist_free_all (list);
17381802 }
17391803 }
17401804 // After the loop, not inside it: cleaning up per host left every later
17411805 // iteration using a freed handle.
17421806 curl_easy_cleanup (curl);
1743- curl_slist_free_all (list);
17441807 }
17451808 return ret;
17461809}
@@ -1802,7 +1865,7 @@ void CcdbApi::scheduleDownload(RequestContext& requestContext, size_t* requestCo
18021865 std::string fullUrl = getFullUrlForRetrieval (curl_handle, requestContext.path , requestContext.metadata , requestContext.timestamp );
18031866 curl_slist* options_list = nullptr ;
18041867 initCurlHTTPHeaderOptionsForRetrieve (curl_handle, options_list, requestContext.timestamp , &requestContext.headers ,
1805- requestContext.etag , requestContext.createdNotAfter , requestContext.createdNotBefore );
1868+ requestContext.etag , requestContext.createdNotAfter , requestContext.createdNotBefore , fullUrl );
18061869
18071870 data->headers = &requestContext.headers ;
18081871 data->hosts = hostsPool;
0 commit comments