Grok 20.3.2
S3Fetcher.h
Go to the documentation of this file.
1/*
2 * Copyright (C) 2016-2026 Grok Image Compression Inc.
3 *
4 * This source code is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License, version 3,
6 * as published by the Free Software Foundation.
7 *
8 * This source code is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Affero General Public License for more details.
12 *
13 * You should have received a copy of the GNU Affero General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18#pragma once
19
20#include <cstdlib>
21#include <cstdio>
22#include <ctime>
23#include <string>
24#include <mutex>
25#include <fstream>
26#include <filesystem>
27
28#include "grk_config_private.h"
29#include "CurlFetcher.h"
30#include "FetchPathParser.h"
31#include "IniParser.h"
32
33#ifdef GRK_ENABLE_LIBCURL
34
35namespace grk
36{
37
169
170// Credential source tracking (mirrors GDAL's AWSCredentialsSource)
171enum class AWSCredentialSource
172{
173 NONE,
174 NO_SIGN_REQUEST,
175 ENVIRONMENT,
176 CONFIG_FILE,
177 WEB_IDENTITY,
178 ASSUMED_ROLE,
179 SSO,
180 CREDENTIAL_PROCESS,
181 EC2_OR_ECS
182};
183
190class S3Fetcher : public CurlFetcher
191{
192 // Cached credentials shared across all S3Fetcher instances.
193 // Temporary credentials (STS, SSO, EC2, etc.) are cached until expiration
194 // with a 60-second safety margin, matching GDAL behavior.
195 struct CredentialCache
196 {
197 std::mutex mutex;
198 AWSCredentialSource source = AWSCredentialSource::NONE;
199 std::string accessKey;
200 std::string secretKey;
201 std::string sessionToken;
202 std::string region;
203 time_t expiration = 0;
204
205 // State for credential refresh
206 std::string roleArn;
207 std::string webIdentityTokenFile;
208 std::string ssoStartURL;
209 std::string ssoAccountId;
210 std::string ssoRoleName;
211 std::string credentialProcess;
212 std::string sourceAccessKey;
213 std::string sourceSecretKey;
214 std::string sourceSessionToken;
215 std::string externalId;
216 std::string roleSessionName;
217
218 bool isValid() const
219 {
220 if(accessKey.empty() || secretKey.empty())
221 return false;
222 if(expiration > 0)
223 {
224 time_t now;
225 time(&now);
226 return now < expiration - 60;
227 }
228 return true;
229 }
230 };
231
232 static CredentialCache& cache()
233 {
234 static CredentialCache instance;
235 return instance;
236 }
237
238protected:
239 void parse(const std::string& path) override
240 {
241 bool is_streaming = path.starts_with("/vsis3_streaming/");
242 grklog.debug("S3Fetcher: parsing path: %s (streaming: %s)", path.c_str(),
243 is_streaming ? "true" : "false");
244
245 configureAuth();
246 bool use_virtual_hosting = isVirtualHostingEnabled();
247
248 ParsedFetchPath parsed;
249 std::string url_or_vsi = path;
250
251 if(url_or_vsi.starts_with("/vsis3/") || is_streaming)
252 {
253 FetchPathParser::parseVsiPath(url_or_vsi, parsed, is_streaming ? "vsis3_streaming" : "vsis3");
254 configureEndpoint(parsed, use_virtual_hosting);
255 }
256 else if(url_or_vsi.starts_with("https://") || url_or_vsi.starts_with("http://"))
257 {
258 bool is_virtual_host = handleVirtualHosting(url_or_vsi, parsed);
259 if(!is_virtual_host)
260 {
261 if(url_or_vsi.starts_with("https://"))
262 FetchPathParser::parseHttpsPath(url_or_vsi, parsed);
263 else
264 parseHttpUrl(url_or_vsi, parsed);
265 }
266 }
267 else
268 {
269 grklog.error("Unsupported URL format: %s", url_or_vsi.c_str());
270 throw std::runtime_error("Unsupported URL format");
271 }
272
273 grklog.debug("S3Fetcher: parsed - Host: %s, Port: %d, Bucket: %s, Key: %s", parsed.host.c_str(),
274 parsed.port, parsed.bucket.c_str(), parsed.key.c_str());
275
276 bool use_https = useHttps();
277
278 if((!auth_.s3_endpoint_.empty() || EnvVarManager::get("AWS_S3_ENDPOINT")) &&
279 !use_virtual_hosting)
280 {
281 url_ = (use_https ? "https://" : "http://") + parsed.host +
282 formatPort(parsed.port, use_https) + "/" + parsed.bucket + "/" + parsed.key;
283 }
284 else
285 {
286 url_ = (use_https ? "https://" : "http://") + parsed.host +
287 formatPort(parsed.port, use_https) + "/" + parsed.key;
288 }
289 grklog.debug("S3Fetcher: constructed URL: %s", url_.c_str());
290 }
291
292 void auth(CURL* curl) override
293 {
294 CurlFetcher::auth(curl);
295
296 if(noSignRequest_)
297 {
298 grklog.debug("S3Fetcher: skipping SigV4 signing (AWS_NO_SIGN_REQUEST)");
299 return;
300 }
301
302 // SigV4 signing via libcurl
303 std::string sigv4 = "aws:amz:" + auth_.region_ + ":s3";
304 curl_easy_setopt(curl, CURLOPT_AWS_SIGV4, sigv4.c_str());
305
306 // SSL verification
307 applyInsecureSSL(curl);
308
309 // Connection reuse
310 auto nonCached = EnvVarManager::get("GRK_CURL_NON_CACHED");
311 if(nonCached && nonCached->find("/vsis3/") != std::string::npos)
312 {
313 curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);
314 }
315
316 // Timeout
317 long timeout = EnvVarManager::get_int("GRK_CURL_TIMEOUT", 0);
318 if(timeout > 0)
319 curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
320
321 // Buffer size
322 long bufSize = EnvVarManager::get_int("GRK_CURL_CACHE_SIZE", 0);
323 if(bufSize > 0)
324 curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, bufSize);
325 }
326
327 curl_slist* prepareAuthHeaders(curl_slist* headers) override
328 {
329 if(noSignRequest_)
330 return headers;
331
332 time_t now;
333 time(&now);
334 char date_buf[64];
335 strftime(date_buf, sizeof(date_buf), "%Y%m%dT%H%M%SZ", gmtime(&now));
336 headers = curl_slist_append(headers, ("x-amz-date: " + std::string(date_buf)).c_str());
337
338 if(!auth_.session_token_.empty())
339 {
340 headers =
341 curl_slist_append(headers, ("x-amz-security-token: " + auth_.session_token_).c_str());
342 }
343
344 if(!requestPayer_.empty())
345 {
346 headers = curl_slist_append(headers, ("x-amz-request-payer: " + requestPayer_).c_str());
347 }
348
349 return headers;
350 }
351
352private:
353 bool noSignRequest_ = false;
354 std::string requestPayer_;
355
356 // ═══════════════════════════════════════════════════════════════════
357 // Credential chain (matches GDAL precedence order)
358 // ═══════════════════════════════════════════════════════════════════
359
360 void configureAuth()
361 {
362 // Requester pays: prefer FetchAuth value (from GDAL), fall back to env var
363 if(!auth_.request_payer_.empty())
364 requestPayer_ = auth_.request_payer_;
365 else
366 requestPayer_ = EnvVarManager::get_string("AWS_REQUEST_PAYER");
367
368 // AWS_NO_SIGN_REQUEST — anonymous access for public buckets
369 if(auth_.s3_no_sign_request_ || EnvVarManager::test_bool("AWS_NO_SIGN_REQUEST"))
370 {
371 noSignRequest_ = true;
372 resolveRegion("default");
373 grklog.debug("S3Fetcher: unsigned requests (AWS_NO_SIGN_REQUEST)");
374 return;
375 }
376
377 // 0. Pre-configured credentials (e.g. passed by GDAL after resolving its own auth chain)
378 if(!auth_.username_.empty() && !auth_.password_.empty())
379 {
380 if(auth_.region_.empty())
381 resolveRegion("default");
382 grklog.debug("S3Fetcher: credentials from pre-configured auth");
383 return;
384 }
385
386 // 1. Environment variables (highest precedence)
387 if(tryEnvCredentials())
388 {
389 grklog.debug("S3Fetcher: credentials from environment variables");
390 return;
391 }
392
393 // 2. Previously cached temporary credentials
394 if(tryCachedCredentials())
395 {
396 grklog.debug("S3Fetcher: credentials from cache");
397 return;
398 }
399
400 // 3. AWS config/credentials files
401 std::string profile = getProfile();
402 if(tryConfigFileCredentials(profile))
403 {
404 grklog.debug("S3Fetcher: credentials from config files");
405 return;
406 }
407
408 // 4. Web Identity Token from env vars (EKS/IRSA)
409 if(EnvVarManager::test_bool("GRK_AWS_WEB_IDENTITY_ENABLE", true))
410 {
411 if(tryWebIdentityToken())
412 {
413 grklog.debug("S3Fetcher: credentials from Web Identity Token");
414 return;
415 }
416 }
417
418 // 5. ECS container credentials
419 if(tryECSCredentials())
420 {
421 grklog.debug("S3Fetcher: credentials from ECS container");
422 return;
423 }
424
425 // 6. EC2 instance metadata (IMDSv2 with v1 fallback)
426 if(!EnvVarManager::test_bool("GRK_AWS_AUTODETECT_EC2_DISABLE"))
427 {
428 if(tryEC2InstanceMetadata())
429 {
430 grklog.debug("S3Fetcher: credentials from EC2 instance metadata");
431 return;
432 }
433 }
434
435 grklog.warn("S3Fetcher: no valid AWS credentials found. "
436 "Set AWS_SECRET_ACCESS_KEY/AWS_ACCESS_KEY_ID, configure ~/.aws/credentials, "
437 "or set AWS_NO_SIGN_REQUEST=YES for public buckets.");
438 }
439
440 // ─── 1. Environment variables ─────────────────────────────────────
441
442 bool tryEnvCredentials()
443 {
444 auto secretKey = EnvVarManager::get("AWS_SECRET_ACCESS_KEY");
445 if(!secretKey || secretKey->empty())
446 return false;
447
448 auto accessKey = EnvVarManager::get("AWS_ACCESS_KEY_ID");
449 if(!accessKey || accessKey->empty())
450 {
451 grklog.warn("S3Fetcher: AWS_SECRET_ACCESS_KEY set but AWS_ACCESS_KEY_ID missing");
452 return false;
453 }
454
455 auth_.username_ = *accessKey;
456 auth_.password_ = *secretKey;
457 auth_.session_token_ = EnvVarManager::get_string("AWS_SESSION_TOKEN");
458 resolveRegion("default");
459 return true;
460 }
461
462 // ─── 2. Cached temporary credentials ──────────────────────────────
463
464 bool tryCachedCredentials()
465 {
466 auto& c = cache();
467 std::lock_guard<std::mutex> lock(c.mutex);
468
469 if(c.source == AWSCredentialSource::NONE || !c.isValid())
470 return false;
471
472 auth_.username_ = c.accessKey;
473 auth_.password_ = c.secretKey;
474 auth_.session_token_ = c.sessionToken;
475 if(!c.region.empty())
476 auth_.region_ = c.region;
477 else
478 resolveRegion(getProfile());
479 return true;
480 }
481
482 // ─── 3. AWS config/credentials files ──────────────────────────────
483
484 bool tryConfigFileCredentials(const std::string& profile)
485 {
486 resolveRegion(profile);
487
488 // Read ~/.aws/credentials
489 std::string credFile = getCredentialsFilePath();
490 if(!credFile.empty())
491 {
492 IniParser parser;
493 if(parser.parse(credFile))
494 {
495 auto it = parser.sections.find(profile);
496 if(it != parser.sections.end())
497 {
498 auto& section = it->second;
499 auto keyIt = section.find("aws_access_key_id");
500 auto secretIt = section.find("aws_secret_access_key");
501 if(keyIt != section.end() && secretIt != section.end() && !keyIt->second.empty() &&
502 !secretIt->second.empty())
503 {
504 auth_.username_ = keyIt->second;
505 auth_.password_ = secretIt->second;
506 auto tokenIt = section.find("aws_session_token");
507 if(tokenIt != section.end())
508 auth_.session_token_ = tokenIt->second;
509 grklog.debug("S3Fetcher: found credentials in profile '%s'", profile.c_str());
510 return true;
511 }
512 }
513 }
514 }
515
516 // Read ~/.aws/config for advanced credential sources
517 std::string configFile = getConfigFilePath();
518 if(configFile.empty())
519 return false;
520
521 IniParser parser;
522 if(!parser.parse(configFile))
523 return false;
524
525 // Find the profile section
526 auto it = parser.sections.find("profile " + profile);
527 if(it == parser.sections.end())
528 it = parser.sections.find(profile);
529 if(it == parser.sections.end())
530 return false;
531
532 auto& section = it->second;
533
534 // ── role_arn → STS AssumeRole or WebIdentity ──
535 auto roleArnIt = section.find("role_arn");
536 if(roleArnIt != section.end() && !roleArnIt->second.empty())
537 {
538 std::string roleArn = roleArnIt->second;
539
540 // Web Identity via config (role_arn + web_identity_token_file)
541 auto tokenFileIt = section.find("web_identity_token_file");
542 if(tokenFileIt != section.end() && !tokenFileIt->second.empty())
543 {
544 if(tryWebIdentityToken(roleArn, tokenFileIt->second))
545 return true;
546 }
547
548 // Assume Role via source_profile
549 auto sourceProfileIt = section.find("source_profile");
550 if(sourceProfileIt != section.end() && !sourceProfileIt->second.empty())
551 {
552 std::string externalId;
553 auto extIt = section.find("external_id");
554 if(extIt != section.end())
555 externalId = extIt->second;
556
557 std::string sessionName =
558 EnvVarManager::get_string("AWS_ROLE_SESSION_NAME", "grok-session");
559 auto sessIt = section.find("role_session_name");
560 if(sessIt != section.end())
561 sessionName = sessIt->second;
562
563 // Check if source_profile uses web identity
564 auto spIt = parser.sections.find("profile " + sourceProfileIt->second);
565 if(spIt == parser.sections.end())
566 spIt = parser.sections.find(sourceProfileIt->second);
567 if(spIt != parser.sections.end())
568 {
569 auto spTokenIt = spIt->second.find("web_identity_token_file");
570 auto spRoleIt = spIt->second.find("role_arn");
571 if(spTokenIt != spIt->second.end() && spRoleIt != spIt->second.end() &&
572 !spTokenIt->second.empty() && !spRoleIt->second.empty())
573 {
574 if(tryWebIdentityToken(spRoleIt->second, spTokenIt->second))
575 {
576 if(trySTSAssumeRole(roleArn, externalId, sessionName))
577 return true;
578 }
579 }
580 }
581
582 // Read source profile credentials directly
583 if(readProfileCredentials(sourceProfileIt->second))
584 {
585 if(trySTSAssumeRole(roleArn, externalId, sessionName))
586 return true;
587 }
588 }
589 }
590
591 // ── SSO ──
592 auto ssoStartIt = section.find("sso_start_url");
593 auto ssoSessionIt = section.find("sso_session");
594 if((ssoStartIt != section.end() && !ssoStartIt->second.empty()) ||
595 (ssoSessionIt != section.end() && !ssoSessionIt->second.empty()))
596 {
597 std::string startUrl = ssoStartIt != section.end() ? ssoStartIt->second : "";
598 std::string ssoSession = ssoSessionIt != section.end() ? ssoSessionIt->second : "";
599
600 std::string accountId, roleName;
601 auto accIt = section.find("sso_account_id");
602 if(accIt != section.end())
603 accountId = accIt->second;
604 auto roleIt = section.find("sso_role_name");
605 if(roleIt != section.end())
606 roleName = roleIt->second;
607
608 // Resolve sso_session → start URL from [sso-session X] section
609 if(!ssoSession.empty() && startUrl.empty())
610 {
611 auto sessSecIt = parser.sections.find("sso-session " + ssoSession);
612 if(sessSecIt != parser.sections.end())
613 {
614 auto urlIt = sessSecIt->second.find("sso_start_url");
615 if(urlIt != sessSecIt->second.end())
616 startUrl = urlIt->second;
617 }
618 }
619
620 if(trySSOCredentials(startUrl, ssoSession, accountId, roleName))
621 return true;
622 }
623
624 // ── credential_process ──
625 auto credProcIt = section.find("credential_process");
626 if(credProcIt != section.end() && !credProcIt->second.empty())
627 {
628 if(tryCredentialProcess(credProcIt->second))
629 return true;
630 }
631
632 return false;
633 }
634
635 bool readProfileCredentials(const std::string& profile)
636 {
637 std::string credFile = getCredentialsFilePath();
638 if(credFile.empty())
639 return false;
640
641 IniParser parser;
642 if(!parser.parse(credFile))
643 return false;
644
645 auto it = parser.sections.find(profile);
646 if(it == parser.sections.end())
647 return false;
648
649 auto keyIt = it->second.find("aws_access_key_id");
650 auto secretIt = it->second.find("aws_secret_access_key");
651 if(keyIt == it->second.end() || secretIt == it->second.end() || keyIt->second.empty() ||
652 secretIt->second.empty())
653 return false;
654
655 auth_.username_ = keyIt->second;
656 auth_.password_ = secretIt->second;
657 auto tokenIt = it->second.find("aws_session_token");
658 if(tokenIt != it->second.end())
659 auth_.session_token_ = tokenIt->second;
660 return true;
661 }
662
663 // ─── 4. Web Identity Token (IRSA/OIDC/EKS) ───────────────────────
664
665 bool tryWebIdentityToken(const std::string& roleArnIn = "", const std::string& tokenFileIn = "")
666 {
667 std::string roleArn =
668 !roleArnIn.empty() ? roleArnIn : EnvVarManager::get_string("AWS_ROLE_ARN");
669 if(roleArn.empty())
670 return false;
671
672 std::string tokenFile = !tokenFileIn.empty()
673 ? tokenFileIn
674 : EnvVarManager::get_string("AWS_WEB_IDENTITY_TOKEN_FILE");
675 if(tokenFile.empty())
676 return false;
677
678 std::string token;
679 if(!readFileContents(tokenFile, token) || token.empty())
680 {
681 grklog.warn("S3Fetcher: cannot read web identity token file: %s", tokenFile.c_str());
682 return false;
683 }
684 trimWhitespace(token);
685
686 // STS endpoint
687 std::string stsUrl = buildSTSUrl();
688 std::string sessionName = EnvVarManager::get_string("AWS_ROLE_SESSION_NAME", "grok-session");
689
690 std::string requestUrl = stsUrl +
691 "/?Action=AssumeRoleWithWebIdentity"
692 "&RoleSessionName=" +
693 urlEncode(sessionName) +
694 "&Version=2011-06-15"
695 "&RoleArn=" +
696 urlEncode(roleArn) + "&WebIdentityToken=" + urlEncode(token);
697
698 std::string response = curlGet(requestUrl);
699 if(response.empty())
700 {
701 grklog.warn("S3Fetcher: STS AssumeRoleWithWebIdentity request failed");
702 return false;
703 }
704
705 std::string accessKey = extractXmlValue(response, "AccessKeyId");
706 std::string secretKey = extractXmlValue(response, "SecretAccessKey");
707 std::string sessionToken = extractXmlValue(response, "SessionToken");
708 std::string expiration = extractXmlValue(response, "Expiration");
709
710 if(accessKey.empty() || secretKey.empty() || sessionToken.empty())
711 {
712 grklog.warn("S3Fetcher: STS AssumeRoleWithWebIdentity returned incomplete credentials");
713 return false;
714 }
715
716 auth_.username_ = accessKey;
717 auth_.password_ = secretKey;
718 auth_.session_token_ = sessionToken;
719 resolveRegion(getProfile());
720
721 cacheCredentials(AWSCredentialSource::WEB_IDENTITY, accessKey, secretKey, sessionToken,
722 parseIso8601(expiration));
723 auto& c = cache();
724 std::lock_guard<std::mutex> lock(c.mutex);
725 c.roleArn = roleArn;
726 c.webIdentityTokenFile = tokenFile;
727
728 grklog.debug("S3Fetcher: cached web identity credentials until %s", expiration.c_str());
729 return true;
730 }
731
732 // ─── STS AssumeRole ───────────────────────────────────────────────
733
734 bool trySTSAssumeRole(const std::string& roleArn, const std::string& externalId,
735 const std::string& sessionName)
736 {
737 if(roleArn.empty() || auth_.username_.empty() || auth_.password_.empty())
738 {
739 grklog.warn("S3Fetcher: cannot assume role without source credentials");
740 return false;
741 }
742
743 std::string sourceAccessKey = auth_.username_;
744 std::string sourceSecretKey = auth_.password_;
745 std::string sourceSessionToken = auth_.session_token_;
746
747 std::string region = auth_.region_.empty() ? "us-east-1" : auth_.region_;
748 std::string stsUrl = buildSTSUrl();
749
750 // Build request parameters
751 std::string params = "Action=AssumeRole"
752 "&RoleArn=" +
753 urlEncode(roleArn) + "&RoleSessionName=" + urlEncode(sessionName) +
754 "&Version=2011-06-15";
755 if(!externalId.empty())
756 params += "&ExternalId=" + urlEncode(externalId);
757
758 // Make signed STS request with source credentials
759 CURL* curl = curl_easy_init();
760 if(!curl)
761 return false;
762
763 std::string response;
764 std::string fullUrl = stsUrl + "/?" + params;
765 curl_easy_setopt(curl, CURLOPT_URL, fullUrl.c_str());
766 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlFetcher::writeCallback);
767 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
768 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
769 applyInsecureSSL(curl);
770 curl_easy_setopt(curl, CURLOPT_USERNAME, sourceAccessKey.c_str());
771 curl_easy_setopt(curl, CURLOPT_PASSWORD, sourceSecretKey.c_str());
772
773 std::string sigv4 = "aws:amz:" + region + ":sts";
774 curl_easy_setopt(curl, CURLOPT_AWS_SIGV4, sigv4.c_str());
775
776 struct curl_slist* headers = nullptr;
777 if(!sourceSessionToken.empty())
778 {
779 headers = curl_slist_append(headers, ("x-amz-security-token: " + sourceSessionToken).c_str());
780 }
781 time_t now;
782 time(&now);
783 char date_buf[64];
784 strftime(date_buf, sizeof(date_buf), "%Y%m%dT%H%M%SZ", gmtime(&now));
785 headers = curl_slist_append(headers, ("x-amz-date: " + std::string(date_buf)).c_str());
786 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
787
788 CURLcode res = curl_easy_perform(curl);
789 curl_slist_free_all(headers);
790 curl_easy_cleanup(curl);
791
792 if(res != CURLE_OK)
793 {
794 grklog.warn("S3Fetcher: STS AssumeRole request failed: %s", curl_easy_strerror(res));
795 return false;
796 }
797
798 std::string accessKey = extractXmlValue(response, "AccessKeyId");
799 std::string secretKey = extractXmlValue(response, "SecretAccessKey");
800 std::string sessionToken = extractXmlValue(response, "SessionToken");
801 std::string expiration = extractXmlValue(response, "Expiration");
802
803 if(accessKey.empty() || secretKey.empty() || sessionToken.empty())
804 {
805 grklog.warn("S3Fetcher: STS AssumeRole returned incomplete credentials");
806 return false;
807 }
808
809 auth_.username_ = accessKey;
810 auth_.password_ = secretKey;
811 auth_.session_token_ = sessionToken;
812
813 cacheCredentials(AWSCredentialSource::ASSUMED_ROLE, accessKey, secretKey, sessionToken,
814 parseIso8601(expiration));
815 {
816 auto& c = cache();
817 std::lock_guard<std::mutex> lock(c.mutex);
818 c.roleArn = roleArn;
819 c.externalId = externalId;
820 c.roleSessionName = sessionName;
821 c.sourceAccessKey = sourceAccessKey;
822 c.sourceSecretKey = sourceSecretKey;
823 c.sourceSessionToken = sourceSessionToken;
824 }
825
826 grklog.debug("S3Fetcher: assumed role %s", roleArn.c_str());
827 return true;
828 }
829
830 // ─── 5. ECS container credentials ─────────────────────────────────
831
832 bool tryECSCredentials()
833 {
834 auto fullUri = EnvVarManager::get("AWS_CONTAINER_CREDENTIALS_FULL_URI");
835 auto relativeUri = EnvVarManager::get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI");
836
837 if((!fullUri || fullUri->empty()) && (!relativeUri || relativeUri->empty()))
838 return false;
839
840 std::string credUrl;
841 if(fullUri && !fullUri->empty())
842 credUrl = *fullUri;
843 else
844 credUrl = "http://169.254.170.2" + *relativeUri;
845
846 // Authorization token
847 std::string authHeader;
848 auto tokenFile = EnvVarManager::get("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE");
849 auto tokenValue = EnvVarManager::get("AWS_CONTAINER_AUTHORIZATION_TOKEN");
850
851 if(tokenFile && !tokenFile->empty())
852 {
853 std::string token;
854 if(readFileContents(*tokenFile, token) && !token.empty())
855 {
856 trimWhitespace(token);
857 authHeader = "Authorization: " + token;
858 }
859 }
860 else if(tokenValue && !tokenValue->empty())
861 {
862 authHeader = "Authorization: " + *tokenValue;
863 }
864
865 std::string response = curlGet(credUrl, authHeader);
866 if(response.empty())
867 {
868 grklog.debug("S3Fetcher: ECS credential fetch failed");
869 return false;
870 }
871
872 // ECS returns JSON
873 std::string accessKey = extractJsonString(response, "AccessKeyId");
874 std::string secretKey = extractJsonString(response, "SecretAccessKey");
875 std::string sessionToken = extractJsonString(response, "Token");
876 std::string expiration = extractJsonString(response, "Expiration");
877
878 if(accessKey.empty() || secretKey.empty())
879 {
880 grklog.debug("S3Fetcher: ECS credentials incomplete");
881 return false;
882 }
883
884 auth_.username_ = accessKey;
885 auth_.password_ = secretKey;
886 auth_.session_token_ = sessionToken;
887 resolveRegion(getProfile());
888
889 cacheCredentials(AWSCredentialSource::EC2_OR_ECS, accessKey, secretKey, sessionToken,
890 parseIso8601(expiration));
891 return true;
892 }
893
894 // ─── 6. EC2 instance metadata (IMDSv2 → v1 fallback) ─────────────
895
896 bool tryEC2InstanceMetadata()
897 {
898 std::string ec2Root =
899 EnvVarManager::get_string("GRK_AWS_EC2_API_ROOT_URL", "http://169.254.169.254");
900
901 // IMDSv2: get session token via PUT
902 std::string imdsToken;
903 {
904 CURL* curl = curl_easy_init();
905 if(!curl)
906 return false;
907
908 std::string tokenUrl = ec2Root + "/latest/api/token";
909 struct curl_slist* headers = nullptr;
910 headers = curl_slist_append(headers, "X-aws-ec2-metadata-token-ttl-seconds: 21600");
911
912 curl_easy_setopt(curl, CURLOPT_URL, tokenUrl.c_str());
913 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
914 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
915 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlFetcher::writeCallback);
916 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &imdsToken);
917 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L);
918
919 CURLcode res = curl_easy_perform(curl);
920 curl_slist_free_all(headers);
921 curl_easy_cleanup(curl);
922
923 if(res != CURLE_OK)
924 {
925 grklog.debug("S3Fetcher: IMDSv2 token request failed, trying IMDSv1 fallback");
926 imdsToken.clear();
927
928 // Verify metadata service is reachable at all (IMDSv1)
929 std::string testResp;
930 CURL* curl2 = curl_easy_init();
931 if(curl2)
932 {
933 curl_easy_setopt(curl2, CURLOPT_URL, (ec2Root + "/latest/meta-data").c_str());
934 curl_easy_setopt(curl2, CURLOPT_WRITEFUNCTION, CurlFetcher::writeCallback);
935 curl_easy_setopt(curl2, CURLOPT_WRITEDATA, &testResp);
936 curl_easy_setopt(curl2, CURLOPT_TIMEOUT, 1L);
937 res = curl_easy_perform(curl2);
938 curl_easy_cleanup(curl2);
939 if(res != CURLE_OK)
940 {
941 grklog.debug("S3Fetcher: not running on EC2 (metadata service unreachable)");
942 return false;
943 }
944 grklog.debug("S3Fetcher: IMDSv2 unavailable, using IMDSv1");
945 }
946 }
947 }
948
949 // Get IAM role name
950 std::string roleUrl = ec2Root + "/latest/meta-data/iam/security-credentials/";
951 std::string roleName = curlGetWithToken(roleUrl, imdsToken, 1);
952 if(roleName.empty())
953 {
954 grklog.debug("S3Fetcher: no IAM role found on instance");
955 return false;
956 }
957 trimWhitespace(roleName);
958
959 // Get credentials for role
960 std::string response = curlGetWithToken(roleUrl + roleName, imdsToken, 5);
961 if(response.empty())
962 {
963 grklog.debug("S3Fetcher: failed to get EC2 IAM credentials");
964 return false;
965 }
966
967 // EC2 metadata returns JSON
968 std::string accessKey = extractJsonString(response, "AccessKeyId");
969 std::string secretKey = extractJsonString(response, "SecretAccessKey");
970 std::string sessionToken = extractJsonString(response, "Token");
971 std::string expiration = extractJsonString(response, "Expiration");
972
973 if(accessKey.empty() || secretKey.empty())
974 {
975 grklog.debug("S3Fetcher: EC2 metadata returned incomplete credentials");
976 return false;
977 }
978
979 auth_.username_ = accessKey;
980 auth_.password_ = secretKey;
981 auth_.session_token_ = sessionToken;
982 resolveRegion(getProfile());
983
984 cacheCredentials(AWSCredentialSource::EC2_OR_ECS, accessKey, secretKey, sessionToken,
985 parseIso8601(expiration));
986
987 grklog.debug("S3Fetcher: obtained EC2 credentials, expiration: %s", expiration.c_str());
988 return true;
989 }
990
991 // ─── SSO credentials ──────────────────────────────────────────────
992
993 bool trySSOCredentials(const std::string& startUrl, const std::string& ssoSession,
994 const std::string& accountId, const std::string& roleName)
995 {
996 if(accountId.empty() || roleName.empty())
997 {
998 grklog.warn("S3Fetcher: SSO requires sso_account_id and sso_role_name");
999 return false;
1000 }
1001
1002 std::string awsDir = getAWSRootDir();
1003 if(awsDir.empty())
1004 return false;
1005
1006 std::string cacheDir = awsDir + "/sso/cache";
1007 std::string accessToken;
1008 std::string ssoRegion;
1009
1010 // Scan SSO cache for a file with matching startUrl
1011 try
1012 {
1013 for(auto& entry : std::filesystem::directory_iterator(cacheDir))
1014 {
1015 if(!entry.is_regular_file() || entry.path().extension() != ".json")
1016 continue;
1017
1018 std::string contents;
1019 if(!readFileContents(entry.path().string(), contents))
1020 continue;
1021
1022 std::string gotStartUrl = extractJsonString(contents, "startUrl");
1023 if(gotStartUrl.empty())
1024 continue;
1025
1026 bool match = false;
1027 if(!startUrl.empty() && gotStartUrl == startUrl)
1028 match = true;
1029 if(!ssoSession.empty() && !startUrl.empty() && gotStartUrl == startUrl)
1030 match = true;
1031
1032 if(!match)
1033 continue;
1034
1035 // Verify token hasn't expired
1036 std::string expiresAt = extractJsonString(contents, "expiresAt");
1037 if(!expiresAt.empty())
1038 {
1039 time_t expTime = parseIso8601(expiresAt);
1040 time_t now;
1041 time(&now);
1042 if(now > expTime)
1043 {
1044 grklog.warn("S3Fetcher: SSO token expired at %s. Run 'aws sso login'.",
1045 expiresAt.c_str());
1046 continue;
1047 }
1048 }
1049
1050 accessToken = extractJsonString(contents, "accessToken");
1051 ssoRegion = extractJsonString(contents, "region");
1052 if(!accessToken.empty())
1053 break;
1054 }
1055 }
1056 catch(const std::exception& e)
1057 {
1058 grklog.debug("S3Fetcher: cannot scan SSO cache: %s", e.what());
1059 return false;
1060 }
1061
1062 if(accessToken.empty())
1063 {
1064 grklog.debug("S3Fetcher: no valid SSO token in cache");
1065 return false;
1066 }
1067
1068 if(ssoRegion.empty())
1069 ssoRegion = "us-east-1";
1070
1071 // GetRoleCredentials request
1072 bool https = useHttps();
1073 std::string defaultHost = "portal.sso." + ssoRegion + ".amazonaws.com";
1074 std::string ssoHost = EnvVarManager::get_string("GRK_AWS_SSO_ENDPOINT", defaultHost);
1075
1076 std::string ssoUrl = (https ? "https://" : "http://") + ssoHost +
1077 "/federation/credentials?role_name=" + urlEncode(roleName) +
1078 "&account_id=" + urlEncode(accountId);
1079
1080 std::string response = curlGet(ssoUrl, "x-amz-sso_bearer_token: " + accessToken);
1081 if(response.empty())
1082 {
1083 grklog.warn("S3Fetcher: SSO GetRoleCredentials failed");
1084 return false;
1085 }
1086
1087 // Response JSON: {"roleCredentials":{"accessKeyId":"...","secretAccessKey":"...", ...}}
1088 std::string ak = extractJsonString(response, "accessKeyId");
1089 std::string sk = extractJsonString(response, "secretAccessKey");
1090 std::string st = extractJsonString(response, "sessionToken");
1091 std::string expirationMs = extractJsonString(response, "expiration");
1092
1093 if(ak.empty() || sk.empty() || st.empty())
1094 {
1095 grklog.warn("S3Fetcher: SSO returned incomplete credentials");
1096 return false;
1097 }
1098
1099 auth_.username_ = ak;
1100 auth_.password_ = sk;
1101 auth_.session_token_ = st;
1102 resolveRegion(getProfile());
1103
1104 time_t expTime = 0;
1105 if(!expirationMs.empty())
1106 {
1107 try
1108 {
1109 expTime = std::stoll(expirationMs) / 1000;
1110 }
1111 catch(...)
1112 {}
1113 }
1114
1115 cacheCredentials(AWSCredentialSource::SSO, ak, sk, st, expTime);
1116 {
1117 auto& c = cache();
1118 std::lock_guard<std::mutex> lock(c.mutex);
1119 c.ssoStartURL = startUrl;
1120 c.ssoAccountId = accountId;
1121 c.ssoRoleName = roleName;
1122 }
1123
1124 grklog.debug("S3Fetcher: obtained SSO credentials");
1125 return true;
1126 }
1127
1128 // ─── credential_process ───────────────────────────────────────────
1129
1130 bool tryCredentialProcess(const std::string& command)
1131 {
1132 if(command.empty())
1133 return false;
1134
1135 grklog.debug("S3Fetcher: executing credential_process: %s", command.c_str());
1136
1137 FILE* pipe = popen(command.c_str(), "r");
1138 if(!pipe)
1139 {
1140 grklog.warn("S3Fetcher: failed to execute credential_process: %s", command.c_str());
1141 return false;
1142 }
1143
1144 std::string output;
1145 char buffer[256];
1146 while(fgets(buffer, sizeof(buffer), pipe))
1147 output += buffer;
1148
1149 int exitCode = pclose(pipe);
1150 if(exitCode != 0)
1151 {
1152 grklog.warn("S3Fetcher: credential_process exited with code %d", exitCode);
1153 return false;
1154 }
1155
1156 if(output.empty())
1157 {
1158 grklog.warn("S3Fetcher: credential_process returned empty output");
1159 return false;
1160 }
1161
1162 std::string version = extractJsonString(output, "Version");
1163 if(version != "1")
1164 {
1165 grklog.warn("S3Fetcher: credential_process Version '%s' unsupported (expected '1')",
1166 version.c_str());
1167 return false;
1168 }
1169
1170 std::string ak = extractJsonString(output, "AccessKeyId");
1171 std::string sk = extractJsonString(output, "SecretAccessKey");
1172 std::string st = extractJsonString(output, "SessionToken");
1173 std::string expiration = extractJsonString(output, "Expiration");
1174
1175 if(ak.empty() || sk.empty())
1176 {
1177 grklog.warn(
1178 "S3Fetcher: credential_process did not return required AccessKeyId/SecretAccessKey");
1179 return false;
1180 }
1181
1182 auth_.username_ = ak;
1183 auth_.password_ = sk;
1184 auth_.session_token_ = st;
1185 resolveRegion(getProfile());
1186
1187 cacheCredentials(AWSCredentialSource::CREDENTIAL_PROCESS, ak, sk, st,
1188 expiration.empty() ? 0 : parseIso8601(expiration));
1189 {
1190 auto& c = cache();
1191 std::lock_guard<std::mutex> lock(c.mutex);
1192 c.credentialProcess = command;
1193 }
1194
1195 grklog.debug("S3Fetcher: obtained credentials from credential_process");
1196 return true;
1197 }
1198
1199 // ═══════════════════════════════════════════════════════════════════
1200 // URL / endpoint helpers
1201 // ═══════════════════════════════════════════════════════════════════
1202
1203 bool useHttps() const
1204 {
1205 if(auth_.s3_use_https_ != 0)
1206 return auth_.s3_use_https_ > 0;
1207 return EnvVarManager::get_string("AWS_HTTPS", "YES") != "NO";
1208 }
1209
1210 static std::string formatPort(int port, bool https)
1211 {
1212 int defaultPort = https ? 443 : 80;
1213 return port != defaultPort ? ":" + std::to_string(port) : "";
1214 }
1215
1216 bool isVirtualHostingEnabled() const
1217 {
1218 if(auth_.s3_use_virtual_hosting_ != 0)
1219 return auth_.s3_use_virtual_hosting_ > 0;
1220 return EnvVarManager::test_bool("AWS_VIRTUAL_HOSTING");
1221 }
1222
1223 void configureEndpoint(ParsedFetchPath& parsed, bool useVirtualHosting)
1224 {
1225 bool https = useHttps();
1226 int defaultPort = https ? 443 : 80;
1227
1228 // Check pre-configured endpoint first, then env var
1229 std::optional<std::string> s3Endpoint;
1230 if(!auth_.s3_endpoint_.empty())
1231 s3Endpoint = auth_.s3_endpoint_;
1232 else
1233 s3Endpoint = EnvVarManager::get("AWS_S3_ENDPOINT");
1234
1235 if(s3Endpoint)
1236 {
1237 std::string endpoint = *s3Endpoint;
1238 if(endpoint.starts_with("https://"))
1239 endpoint = endpoint.substr(8);
1240 else if(endpoint.starts_with("http://"))
1241 endpoint = endpoint.substr(7);
1242
1243 size_t portPos = endpoint.find(':');
1244 if(portPos != std::string::npos)
1245 {
1246 parsed.host = endpoint.substr(0, portPos);
1247 try
1248 {
1249 parsed.port = std::stoi(endpoint.substr(portPos + 1));
1250 }
1251 catch(...)
1252 {
1253 parsed.port = defaultPort;
1254 }
1255 }
1256 else
1257 {
1258 parsed.host = endpoint;
1259 parsed.port = defaultPort;
1260 }
1261
1262 if(useVirtualHosting)
1263 parsed.host = parsed.bucket + "." + parsed.host;
1264 }
1265 else
1266 {
1267 parsed.host = useVirtualHosting ? parsed.bucket + ".s3." + auth_.region_ + ".amazonaws.com"
1268 : "s3." + auth_.region_ + ".amazonaws.com";
1269 parsed.port = defaultPort;
1270 }
1271 }
1272
1273 bool handleVirtualHosting(const std::string& url, ParsedFetchPath& parsed)
1274 {
1275 if(!isVirtualHostingEnabled())
1276 return false;
1277
1278 // Strip protocol
1279 std::string urlBody = url;
1280 if(urlBody.starts_with("https://"))
1281 urlBody = urlBody.substr(8);
1282 else if(urlBody.starts_with("http://"))
1283 urlBody = urlBody.substr(7);
1284
1285 // Extract host
1286 size_t slashPos = urlBody.find('/');
1287 if(slashPos == std::string::npos)
1288 return false;
1289
1290 std::string hostPort = urlBody.substr(0, slashPos);
1291 std::string pathPart = urlBody.substr(slashPos + 1);
1292
1293 // Strip port from host
1294 std::string host = hostPort;
1295 size_t colonPos = hostPort.find(':');
1296 if(colonPos != std::string::npos)
1297 {
1298 host = hostPort.substr(0, colonPos);
1299 try
1300 {
1301 parsed.port = std::stoi(hostPort.substr(colonPos + 1));
1302 }
1303 catch(...)
1304 {
1305 parsed.port = useHttps() ? 443 : 80;
1306 }
1307 }
1308
1309 // Determine S3 endpoint suffix (check pre-configured first, then env var)
1310 std::string s3Endpoint;
1311 if(!auth_.s3_endpoint_.empty())
1312 s3Endpoint = auth_.s3_endpoint_;
1313 else
1314 s3Endpoint =
1315 EnvVarManager::get_string("AWS_S3_ENDPOINT", "s3." + auth_.region_ + ".amazonaws.com");
1316 if(s3Endpoint.starts_with("https://"))
1317 s3Endpoint = s3Endpoint.substr(8);
1318 else if(s3Endpoint.starts_with("http://"))
1319 s3Endpoint = s3Endpoint.substr(7);
1320 colonPos = s3Endpoint.find(':');
1321 if(colonPos != std::string::npos)
1322 s3Endpoint = s3Endpoint.substr(0, colonPos);
1323
1324 // Check if host is bucket.endpoint
1325 if(host.ends_with(s3Endpoint))
1326 {
1327 size_t bucketLen = host.length() - s3Endpoint.length() - 1;
1328 if(bucketLen > 0 && host[bucketLen] == '.')
1329 {
1330 parsed.bucket = host.substr(0, bucketLen);
1331 parsed.host = host;
1332 parsed.key = pathPart;
1333 grklog.debug("S3Fetcher: detected virtual-hosted URL: bucket=%s", parsed.bucket.c_str());
1334 return true;
1335 }
1336 }
1337 return false;
1338 }
1339
1340 // Parse http:// URLs (similar to FetchPathParser::parseHttpsPath but for http)
1341 static void parseHttpUrl(std::string& url, ParsedFetchPath& parsed)
1342 {
1343 if(!url.starts_with("http://"))
1344 throw std::runtime_error("Invalid HTTP URL");
1345 url = url.substr(7);
1346
1347 size_t slashPos = url.find('/');
1348 if(slashPos == std::string::npos)
1349 throw std::runtime_error("Invalid HTTP URL: no path");
1350
1351 std::string hostPort = url.substr(0, slashPos);
1352 std::string path = url.substr(slashPos + 1);
1353
1354 size_t colonPos = hostPort.find(':');
1355 if(colonPos != std::string::npos)
1356 {
1357 parsed.host = hostPort.substr(0, colonPos);
1358 try
1359 {
1360 parsed.port = std::stoi(hostPort.substr(colonPos + 1));
1361 }
1362 catch(...)
1363 {
1364 parsed.port = 80;
1365 }
1366 }
1367 else
1368 {
1369 parsed.host = hostPort;
1370 parsed.port = 80;
1371 }
1372
1373 size_t bucketEnd = path.find('/');
1374 if(bucketEnd == std::string::npos)
1375 throw std::runtime_error("Invalid HTTP URL: no key after bucket");
1376 parsed.bucket = path.substr(0, bucketEnd);
1377 parsed.key = path.substr(bucketEnd + 1);
1378 }
1379
1380 // ═══════════════════════════════════════════════════════════════════
1381 // Utility helpers
1382 // ═══════════════════════════════════════════════════════════════════
1383
1384 void resolveRegion(const std::string& profile)
1385 {
1386 if(auto r = EnvVarManager::get("AWS_REGION"))
1387 {
1388 auth_.region_ = *r;
1389 return;
1390 }
1391 if(auto r = EnvVarManager::get("AWS_DEFAULT_REGION"))
1392 {
1393 auth_.region_ = *r;
1394 return;
1395 }
1396
1397 // Try config file
1398 if(auth_.region_.empty())
1399 {
1400 std::string configFile = getConfigFilePath();
1401 if(!configFile.empty())
1402 {
1403 IniParser parser;
1404 if(parser.parse(configFile))
1405 {
1406 auto it = parser.sections.find("profile " + profile);
1407 if(it == parser.sections.end())
1408 it = parser.sections.find(profile);
1409 if(it != parser.sections.end())
1410 {
1411 auto regionIt = it->second.find("region");
1412 if(regionIt != it->second.end() && !regionIt->second.empty())
1413 {
1414 auth_.region_ = regionIt->second;
1415 return;
1416 }
1417 }
1418 }
1419 }
1420 }
1421
1422 if(auth_.region_.empty())
1423 auth_.region_ = "us-east-1";
1424 }
1425
1426 static std::string getProfile()
1427 {
1428 if(auto p = EnvVarManager::get("AWS_PROFILE"))
1429 return *p;
1430 if(auto p = EnvVarManager::get("AWS_DEFAULT_PROFILE"))
1431 return *p;
1432 return "default";
1433 }
1434
1435 static std::string getConfigFilePath()
1436 {
1437 if(auto p = EnvVarManager::get("AWS_CONFIG_FILE"))
1438 return *p;
1439#ifdef _WIN32
1440 if(auto p = EnvVarManager::get("USERPROFILE"))
1441 return *p + "\\.aws\\config";
1442#else
1443 if(auto p = EnvVarManager::get("HOME"))
1444 return *p + "/.aws/config";
1445#endif
1446 return {};
1447 }
1448
1449 static std::string getCredentialsFilePath()
1450 {
1451 if(auto p = EnvVarManager::get("GRK_AWS_CREDENTIALS_FILE"))
1452 return *p;
1453#ifdef _WIN32
1454 if(auto p = EnvVarManager::get("USERPROFILE"))
1455 return *p + "\\.aws\\credentials";
1456#else
1457 if(auto p = EnvVarManager::get("HOME"))
1458 return *p + "/.aws/credentials";
1459#endif
1460 return {};
1461 }
1462
1463 static std::string getAWSRootDir()
1464 {
1465 if(auto p = EnvVarManager::get("GRK_AWS_ROOT_DIR"))
1466 return *p;
1467#ifdef _WIN32
1468 if(auto p = EnvVarManager::get("USERPROFILE"))
1469 return *p + "\\.aws";
1470#else
1471 if(auto p = EnvVarManager::get("HOME"))
1472 return *p + "/.aws";
1473#endif
1474 return {};
1475 }
1476
1477 std::string buildSTSUrl()
1478 {
1479 std::string regional = EnvVarManager::get_string("AWS_STS_REGIONAL_ENDPOINTS", "regional");
1480 std::string stsUrl;
1481 if(regional == "regional")
1482 {
1483 std::string region = EnvVarManager::get_string(
1484 "AWS_REGION", EnvVarManager::get_string("AWS_DEFAULT_REGION", "us-east-1"));
1485 stsUrl = "https://sts." + region + ".amazonaws.com";
1486 }
1487 else
1488 {
1489 stsUrl = "https://sts.amazonaws.com";
1490 }
1491 if(auto endpoint = EnvVarManager::get("GRK_AWS_STS_ROOT_URL"))
1492 stsUrl = *endpoint;
1493 return stsUrl;
1494 }
1495
1496 void cacheCredentials(AWSCredentialSource source, const std::string& ak, const std::string& sk,
1497 const std::string& st, time_t expiration)
1498 {
1499 auto& c = cache();
1500 std::lock_guard<std::mutex> lock(c.mutex);
1501 c.source = source;
1502 c.accessKey = ak;
1503 c.secretKey = sk;
1504 c.sessionToken = st;
1505 c.region = auth_.region_;
1506 c.expiration = expiration;
1507 }
1508
1509 // ─── HTTP helpers ─────────────────────────────────────────────────
1510
1511 void applyInsecureSSL(CURL* curl)
1512 {
1513 grklog.debug("applyInsecureSSL: s3_allow_insecure_=%d, GRK_CURL_ALLOW_INSECURE=%d",
1514 (int)auth_.s3_allow_insecure_,
1515 (int)EnvVarManager::test_bool("GRK_CURL_ALLOW_INSECURE"));
1516 if(auth_.s3_allow_insecure_ || EnvVarManager::test_bool("GRK_CURL_ALLOW_INSECURE"))
1517 {
1518 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
1519 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
1520 }
1521 }
1522
1523 std::string curlGet(const std::string& url, const std::string& header = "")
1524 {
1525 CURL* curl = curl_easy_init();
1526 if(!curl)
1527 return {};
1528
1529 std::string response;
1530 curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
1531 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlFetcher::writeCallback);
1532 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
1533 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
1534 applyInsecureSSL(curl);
1535
1536 struct curl_slist* headers = nullptr;
1537 if(!header.empty())
1538 {
1539 headers = curl_slist_append(headers, header.c_str());
1540 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
1541 }
1542
1543 CURLcode res = curl_easy_perform(curl);
1544 if(headers)
1545 curl_slist_free_all(headers);
1546 curl_easy_cleanup(curl);
1547
1548 return res == CURLE_OK ? response : std::string{};
1549 }
1550
1551 std::string curlGetWithToken(const std::string& url, const std::string& token, long timeout = 5)
1552 {
1553 CURL* curl = curl_easy_init();
1554 if(!curl)
1555 return {};
1556
1557 std::string response;
1558 curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
1559 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlFetcher::writeCallback);
1560 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
1561 curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
1562 applyInsecureSSL(curl);
1563
1564 struct curl_slist* headers = nullptr;
1565 if(!token.empty())
1566 {
1567 headers = curl_slist_append(headers, ("X-aws-ec2-metadata-token: " + token).c_str());
1568 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
1569 }
1570
1571 CURLcode res = curl_easy_perform(curl);
1572 if(headers)
1573 curl_slist_free_all(headers);
1574 curl_easy_cleanup(curl);
1575
1576 return res == CURLE_OK ? response : std::string{};
1577 }
1578
1579 // ─── Parsing helpers ──────────────────────────────────────────────
1580
1581 static std::string extractXmlValue(const std::string& xml, const std::string& tag)
1582 {
1583 std::string openTag = "<" + tag + ">";
1584 std::string closeTag = "</" + tag + ">";
1585 size_t pos = xml.find(openTag);
1586 if(pos == std::string::npos)
1587 return {};
1588 pos += openTag.length();
1589 size_t end = xml.find(closeTag, pos);
1590 if(end == std::string::npos)
1591 return {};
1592 return xml.substr(pos, end - pos);
1593 }
1594
1595 static std::string extractJsonString(const std::string& json, const std::string& key)
1596 {
1597 std::string pattern = "\"" + key + "\"";
1598 size_t pos = json.find(pattern);
1599 if(pos == std::string::npos)
1600 return {};
1601 pos += pattern.length();
1602
1603 // Skip whitespace and colon
1604 while(pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || json[pos] == ':'))
1605 pos++;
1606
1607 if(pos >= json.size())
1608 return {};
1609
1610 if(json[pos] == '"')
1611 {
1612 // Quoted string value
1613 pos++;
1614 size_t end = pos;
1615 while(end < json.size() && json[end] != '"')
1616 {
1617 if(json[end] == '\\')
1618 end++; // skip escaped char
1619 end++;
1620 }
1621 return json.substr(pos, end - pos);
1622 }
1623
1624 // Numeric or unquoted value
1625 size_t end = pos;
1626 while(end < json.size() && json[end] != ',' && json[end] != '}' && json[end] != ' ' &&
1627 json[end] != '\n')
1628 end++;
1629 return json.substr(pos, end - pos);
1630 }
1631
1632 static bool readFileContents(const std::string& path, std::string& contents)
1633 {
1634 std::ifstream file(path, std::ios::binary);
1635 if(!file.is_open())
1636 return false;
1637 contents.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
1638 return true;
1639 }
1640
1641 static void trimWhitespace(std::string& s)
1642 {
1643 s.erase(0, s.find_first_not_of(" \t\r\n"));
1644 s.erase(s.find_last_not_of(" \t\r\n") + 1);
1645 }
1646
1647 static time_t parseIso8601(const std::string& str)
1648 {
1649 if(str.empty())
1650 return 0;
1651
1652 struct tm tm = {};
1653 if(sscanf(str.c_str(), "%d-%d-%dT%d:%d:%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour,
1654 &tm.tm_min, &tm.tm_sec) >= 6)
1655 {
1656 tm.tm_year -= 1900;
1657 tm.tm_mon -= 1;
1658#ifdef _WIN32
1659 return _mkgmtime(&tm);
1660#else
1661 return timegm(&tm);
1662#endif
1663 }
1664 return 0;
1665 }
1666
1667 static std::string urlEncode(const std::string& str)
1668 {
1669 CURL* curl = curl_easy_init();
1670 if(!curl)
1671 return str;
1672 char* encoded = curl_easy_escape(curl, str.c_str(), (int)str.length());
1673 std::string result(encoded);
1674 curl_free(encoded);
1675 curl_easy_cleanup(curl);
1676 return result;
1677 }
1678};
1679
1680} // namespace grk
1681
1682#endif
ResWindow.
Definition CompressedChunkCache.h:36
ILogger & grklog
Definition Logger.cpp:24
virtual void warn(const char *fmt,...)=0
virtual void error(const char *fmt,...)=0
virtual void debug(const char *fmt,...)=0