28#include "grk_config_private.h"
33#ifdef GRK_ENABLE_LIBCURL
171enum class AWSCredentialSource
190class S3Fetcher :
public CurlFetcher
195 struct CredentialCache
198 AWSCredentialSource source = AWSCredentialSource::NONE;
199 std::string accessKey;
200 std::string secretKey;
201 std::string sessionToken;
203 time_t expiration = 0;
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;
220 if(accessKey.empty() || secretKey.empty())
226 return now < expiration - 60;
232 static CredentialCache& cache()
234 static CredentialCache instance;
239 void parse(
const std::string& path)
override
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");
246 bool use_virtual_hosting = isVirtualHostingEnabled();
248 ParsedFetchPath parsed;
249 std::string url_or_vsi = path;
251 if(url_or_vsi.starts_with(
"/vsis3/") || is_streaming)
253 FetchPathParser::parseVsiPath(url_or_vsi, parsed, is_streaming ?
"vsis3_streaming" :
"vsis3");
254 configureEndpoint(parsed, use_virtual_hosting);
256 else if(url_or_vsi.starts_with(
"https://") || url_or_vsi.starts_with(
"http://"))
258 bool is_virtual_host = handleVirtualHosting(url_or_vsi, parsed);
261 if(url_or_vsi.starts_with(
"https://"))
262 FetchPathParser::parseHttpsPath(url_or_vsi, parsed);
264 parseHttpUrl(url_or_vsi, parsed);
269 grklog.
error(
"Unsupported URL format: %s", url_or_vsi.c_str());
270 throw std::runtime_error(
"Unsupported URL format");
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());
276 bool use_https = useHttps();
278 if((!auth_.s3_endpoint_.empty() || EnvVarManager::get(
"AWS_S3_ENDPOINT")) &&
279 !use_virtual_hosting)
281 url_ = (use_https ?
"https://" :
"http://") + parsed.host +
282 formatPort(parsed.port, use_https) +
"/" + parsed.bucket +
"/" + parsed.key;
286 url_ = (use_https ?
"https://" :
"http://") + parsed.host +
287 formatPort(parsed.port, use_https) +
"/" + parsed.key;
289 grklog.
debug(
"S3Fetcher: constructed URL: %s", url_.c_str());
292 void auth(CURL* curl)
override
294 CurlFetcher::auth(curl);
298 grklog.
debug(
"S3Fetcher: skipping SigV4 signing (AWS_NO_SIGN_REQUEST)");
303 std::string sigv4 =
"aws:amz:" + auth_.region_ +
":s3";
304 curl_easy_setopt(curl, CURLOPT_AWS_SIGV4, sigv4.c_str());
307 applyInsecureSSL(curl);
310 auto nonCached = EnvVarManager::get(
"GRK_CURL_NON_CACHED");
311 if(nonCached && nonCached->find(
"/vsis3/") != std::string::npos)
313 curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);
317 long timeout = EnvVarManager::get_int(
"GRK_CURL_TIMEOUT", 0);
319 curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);
322 long bufSize = EnvVarManager::get_int(
"GRK_CURL_CACHE_SIZE", 0);
324 curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, bufSize);
327 curl_slist* prepareAuthHeaders(curl_slist* headers)
override
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());
338 if(!auth_.session_token_.empty())
341 curl_slist_append(headers, (
"x-amz-security-token: " + auth_.session_token_).c_str());
344 if(!requestPayer_.empty())
346 headers = curl_slist_append(headers, (
"x-amz-request-payer: " + requestPayer_).c_str());
353 bool noSignRequest_ =
false;
354 std::string requestPayer_;
363 if(!auth_.request_payer_.empty())
364 requestPayer_ = auth_.request_payer_;
366 requestPayer_ = EnvVarManager::get_string(
"AWS_REQUEST_PAYER");
369 if(auth_.s3_no_sign_request_ || EnvVarManager::test_bool(
"AWS_NO_SIGN_REQUEST"))
371 noSignRequest_ =
true;
372 resolveRegion(
"default");
373 grklog.
debug(
"S3Fetcher: unsigned requests (AWS_NO_SIGN_REQUEST)");
378 if(!auth_.username_.empty() && !auth_.password_.empty())
380 if(auth_.region_.empty())
381 resolveRegion(
"default");
382 grklog.
debug(
"S3Fetcher: credentials from pre-configured auth");
387 if(tryEnvCredentials())
389 grklog.
debug(
"S3Fetcher: credentials from environment variables");
394 if(tryCachedCredentials())
401 std::string profile = getProfile();
402 if(tryConfigFileCredentials(profile))
404 grklog.
debug(
"S3Fetcher: credentials from config files");
409 if(EnvVarManager::test_bool(
"GRK_AWS_WEB_IDENTITY_ENABLE",
true))
411 if(tryWebIdentityToken())
413 grklog.
debug(
"S3Fetcher: credentials from Web Identity Token");
419 if(tryECSCredentials())
421 grklog.
debug(
"S3Fetcher: credentials from ECS container");
426 if(!EnvVarManager::test_bool(
"GRK_AWS_AUTODETECT_EC2_DISABLE"))
428 if(tryEC2InstanceMetadata())
430 grklog.
debug(
"S3Fetcher: credentials from EC2 instance metadata");
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.");
442 bool tryEnvCredentials()
444 auto secretKey = EnvVarManager::get(
"AWS_SECRET_ACCESS_KEY");
445 if(!secretKey || secretKey->empty())
448 auto accessKey = EnvVarManager::get(
"AWS_ACCESS_KEY_ID");
449 if(!accessKey || accessKey->empty())
451 grklog.
warn(
"S3Fetcher: AWS_SECRET_ACCESS_KEY set but AWS_ACCESS_KEY_ID missing");
455 auth_.username_ = *accessKey;
456 auth_.password_ = *secretKey;
457 auth_.session_token_ = EnvVarManager::get_string(
"AWS_SESSION_TOKEN");
458 resolveRegion(
"default");
464 bool tryCachedCredentials()
467 std::lock_guard<std::mutex> lock(c.mutex);
469 if(c.source == AWSCredentialSource::NONE || !c.isValid())
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;
478 resolveRegion(getProfile());
484 bool tryConfigFileCredentials(
const std::string& profile)
486 resolveRegion(profile);
489 std::string credFile = getCredentialsFilePath();
490 if(!credFile.empty())
493 if(parser.parse(credFile))
495 auto it = parser.sections.find(profile);
496 if(it != parser.sections.end())
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())
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());
517 std::string configFile = getConfigFilePath();
518 if(configFile.empty())
522 if(!parser.parse(configFile))
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())
532 auto& section = it->second;
535 auto roleArnIt = section.find(
"role_arn");
536 if(roleArnIt != section.end() && !roleArnIt->second.empty())
538 std::string roleArn = roleArnIt->second;
541 auto tokenFileIt = section.find(
"web_identity_token_file");
542 if(tokenFileIt != section.end() && !tokenFileIt->second.empty())
544 if(tryWebIdentityToken(roleArn, tokenFileIt->second))
549 auto sourceProfileIt = section.find(
"source_profile");
550 if(sourceProfileIt != section.end() && !sourceProfileIt->second.empty())
552 std::string externalId;
553 auto extIt = section.find(
"external_id");
554 if(extIt != section.end())
555 externalId = extIt->second;
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;
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())
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())
574 if(tryWebIdentityToken(spRoleIt->second, spTokenIt->second))
576 if(trySTSAssumeRole(roleArn, externalId, sessionName))
583 if(readProfileCredentials(sourceProfileIt->second))
585 if(trySTSAssumeRole(roleArn, externalId, sessionName))
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()))
597 std::string startUrl = ssoStartIt != section.end() ? ssoStartIt->second :
"";
598 std::string ssoSession = ssoSessionIt != section.end() ? ssoSessionIt->second :
"";
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;
609 if(!ssoSession.empty() && startUrl.empty())
611 auto sessSecIt = parser.sections.find(
"sso-session " + ssoSession);
612 if(sessSecIt != parser.sections.end())
614 auto urlIt = sessSecIt->second.find(
"sso_start_url");
615 if(urlIt != sessSecIt->second.end())
616 startUrl = urlIt->second;
620 if(trySSOCredentials(startUrl, ssoSession, accountId, roleName))
625 auto credProcIt = section.find(
"credential_process");
626 if(credProcIt != section.end() && !credProcIt->second.empty())
628 if(tryCredentialProcess(credProcIt->second))
635 bool readProfileCredentials(
const std::string& profile)
637 std::string credFile = getCredentialsFilePath();
642 if(!parser.parse(credFile))
645 auto it = parser.sections.find(profile);
646 if(it == parser.sections.end())
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())
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;
665 bool tryWebIdentityToken(
const std::string& roleArnIn =
"",
const std::string& tokenFileIn =
"")
667 std::string roleArn =
668 !roleArnIn.empty() ? roleArnIn : EnvVarManager::get_string(
"AWS_ROLE_ARN");
672 std::string tokenFile = !tokenFileIn.empty()
674 : EnvVarManager::get_string(
"AWS_WEB_IDENTITY_TOKEN_FILE");
675 if(tokenFile.empty())
679 if(!readFileContents(tokenFile, token) || token.empty())
681 grklog.
warn(
"S3Fetcher: cannot read web identity token file: %s", tokenFile.c_str());
684 trimWhitespace(token);
687 std::string stsUrl = buildSTSUrl();
688 std::string sessionName = EnvVarManager::get_string(
"AWS_ROLE_SESSION_NAME",
"grok-session");
690 std::string requestUrl = stsUrl +
691 "/?Action=AssumeRoleWithWebIdentity"
692 "&RoleSessionName=" +
693 urlEncode(sessionName) +
694 "&Version=2011-06-15"
696 urlEncode(roleArn) +
"&WebIdentityToken=" + urlEncode(token);
698 std::string response = curlGet(requestUrl);
701 grklog.
warn(
"S3Fetcher: STS AssumeRoleWithWebIdentity request failed");
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");
710 if(accessKey.empty() || secretKey.empty() || sessionToken.empty())
712 grklog.
warn(
"S3Fetcher: STS AssumeRoleWithWebIdentity returned incomplete credentials");
716 auth_.username_ = accessKey;
717 auth_.password_ = secretKey;
718 auth_.session_token_ = sessionToken;
719 resolveRegion(getProfile());
721 cacheCredentials(AWSCredentialSource::WEB_IDENTITY, accessKey, secretKey, sessionToken,
722 parseIso8601(expiration));
724 std::lock_guard<std::mutex> lock(c.mutex);
726 c.webIdentityTokenFile = tokenFile;
728 grklog.
debug(
"S3Fetcher: cached web identity credentials until %s", expiration.c_str());
734 bool trySTSAssumeRole(
const std::string& roleArn,
const std::string& externalId,
735 const std::string& sessionName)
737 if(roleArn.empty() || auth_.username_.empty() || auth_.password_.empty())
739 grklog.
warn(
"S3Fetcher: cannot assume role without source credentials");
743 std::string sourceAccessKey = auth_.username_;
744 std::string sourceSecretKey = auth_.password_;
745 std::string sourceSessionToken = auth_.session_token_;
747 std::string region = auth_.region_.empty() ?
"us-east-1" : auth_.region_;
748 std::string stsUrl = buildSTSUrl();
751 std::string params =
"Action=AssumeRole"
753 urlEncode(roleArn) +
"&RoleSessionName=" + urlEncode(sessionName) +
754 "&Version=2011-06-15";
755 if(!externalId.empty())
756 params +=
"&ExternalId=" + urlEncode(externalId);
759 CURL* curl = curl_easy_init();
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());
773 std::string sigv4 =
"aws:amz:" + region +
":sts";
774 curl_easy_setopt(curl, CURLOPT_AWS_SIGV4, sigv4.c_str());
776 struct curl_slist* headers =
nullptr;
777 if(!sourceSessionToken.empty())
779 headers = curl_slist_append(headers, (
"x-amz-security-token: " + sourceSessionToken).c_str());
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);
788 CURLcode res = curl_easy_perform(curl);
789 curl_slist_free_all(headers);
790 curl_easy_cleanup(curl);
794 grklog.
warn(
"S3Fetcher: STS AssumeRole request failed: %s", curl_easy_strerror(res));
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");
803 if(accessKey.empty() || secretKey.empty() || sessionToken.empty())
805 grklog.
warn(
"S3Fetcher: STS AssumeRole returned incomplete credentials");
809 auth_.username_ = accessKey;
810 auth_.password_ = secretKey;
811 auth_.session_token_ = sessionToken;
813 cacheCredentials(AWSCredentialSource::ASSUMED_ROLE, accessKey, secretKey, sessionToken,
814 parseIso8601(expiration));
817 std::lock_guard<std::mutex> lock(c.mutex);
819 c.externalId = externalId;
820 c.roleSessionName = sessionName;
821 c.sourceAccessKey = sourceAccessKey;
822 c.sourceSecretKey = sourceSecretKey;
823 c.sourceSessionToken = sourceSessionToken;
826 grklog.
debug(
"S3Fetcher: assumed role %s", roleArn.c_str());
832 bool tryECSCredentials()
834 auto fullUri = EnvVarManager::get(
"AWS_CONTAINER_CREDENTIALS_FULL_URI");
835 auto relativeUri = EnvVarManager::get(
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI");
837 if((!fullUri || fullUri->empty()) && (!relativeUri || relativeUri->empty()))
841 if(fullUri && !fullUri->empty())
844 credUrl =
"http://169.254.170.2" + *relativeUri;
847 std::string authHeader;
848 auto tokenFile = EnvVarManager::get(
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE");
849 auto tokenValue = EnvVarManager::get(
"AWS_CONTAINER_AUTHORIZATION_TOKEN");
851 if(tokenFile && !tokenFile->empty())
854 if(readFileContents(*tokenFile, token) && !token.empty())
856 trimWhitespace(token);
857 authHeader =
"Authorization: " + token;
860 else if(tokenValue && !tokenValue->empty())
862 authHeader =
"Authorization: " + *tokenValue;
865 std::string response = curlGet(credUrl, authHeader);
868 grklog.
debug(
"S3Fetcher: ECS credential fetch failed");
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");
878 if(accessKey.empty() || secretKey.empty())
880 grklog.
debug(
"S3Fetcher: ECS credentials incomplete");
884 auth_.username_ = accessKey;
885 auth_.password_ = secretKey;
886 auth_.session_token_ = sessionToken;
887 resolveRegion(getProfile());
889 cacheCredentials(AWSCredentialSource::EC2_OR_ECS, accessKey, secretKey, sessionToken,
890 parseIso8601(expiration));
896 bool tryEC2InstanceMetadata()
898 std::string ec2Root =
899 EnvVarManager::get_string(
"GRK_AWS_EC2_API_ROOT_URL",
"http://169.254.169.254");
902 std::string imdsToken;
904 CURL* curl = curl_easy_init();
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");
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);
919 CURLcode res = curl_easy_perform(curl);
920 curl_slist_free_all(headers);
921 curl_easy_cleanup(curl);
925 grklog.
debug(
"S3Fetcher: IMDSv2 token request failed, trying IMDSv1 fallback");
929 std::string testResp;
930 CURL* curl2 = curl_easy_init();
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);
941 grklog.
debug(
"S3Fetcher: not running on EC2 (metadata service unreachable)");
944 grklog.
debug(
"S3Fetcher: IMDSv2 unavailable, using IMDSv1");
950 std::string roleUrl = ec2Root +
"/latest/meta-data/iam/security-credentials/";
951 std::string roleName = curlGetWithToken(roleUrl, imdsToken, 1);
954 grklog.
debug(
"S3Fetcher: no IAM role found on instance");
957 trimWhitespace(roleName);
960 std::string response = curlGetWithToken(roleUrl + roleName, imdsToken, 5);
963 grklog.
debug(
"S3Fetcher: failed to get EC2 IAM credentials");
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");
973 if(accessKey.empty() || secretKey.empty())
975 grklog.
debug(
"S3Fetcher: EC2 metadata returned incomplete credentials");
979 auth_.username_ = accessKey;
980 auth_.password_ = secretKey;
981 auth_.session_token_ = sessionToken;
982 resolveRegion(getProfile());
984 cacheCredentials(AWSCredentialSource::EC2_OR_ECS, accessKey, secretKey, sessionToken,
985 parseIso8601(expiration));
987 grklog.
debug(
"S3Fetcher: obtained EC2 credentials, expiration: %s", expiration.c_str());
993 bool trySSOCredentials(
const std::string& startUrl,
const std::string& ssoSession,
994 const std::string& accountId,
const std::string& roleName)
996 if(accountId.empty() || roleName.empty())
998 grklog.
warn(
"S3Fetcher: SSO requires sso_account_id and sso_role_name");
1002 std::string awsDir = getAWSRootDir();
1006 std::string cacheDir = awsDir +
"/sso/cache";
1007 std::string accessToken;
1008 std::string ssoRegion;
1013 for(
auto& entry : std::filesystem::directory_iterator(cacheDir))
1015 if(!entry.is_regular_file() || entry.path().extension() !=
".json")
1018 std::string contents;
1019 if(!readFileContents(entry.path().string(), contents))
1022 std::string gotStartUrl = extractJsonString(contents,
"startUrl");
1023 if(gotStartUrl.empty())
1027 if(!startUrl.empty() && gotStartUrl == startUrl)
1029 if(!ssoSession.empty() && !startUrl.empty() && gotStartUrl == startUrl)
1036 std::string expiresAt = extractJsonString(contents,
"expiresAt");
1037 if(!expiresAt.empty())
1039 time_t expTime = parseIso8601(expiresAt);
1044 grklog.
warn(
"S3Fetcher: SSO token expired at %s. Run 'aws sso login'.",
1050 accessToken = extractJsonString(contents,
"accessToken");
1051 ssoRegion = extractJsonString(contents,
"region");
1052 if(!accessToken.empty())
1056 catch(
const std::exception& e)
1058 grklog.
debug(
"S3Fetcher: cannot scan SSO cache: %s", e.what());
1062 if(accessToken.empty())
1064 grklog.
debug(
"S3Fetcher: no valid SSO token in cache");
1068 if(ssoRegion.empty())
1069 ssoRegion =
"us-east-1";
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);
1076 std::string ssoUrl = (https ?
"https://" :
"http://") + ssoHost +
1077 "/federation/credentials?role_name=" + urlEncode(roleName) +
1078 "&account_id=" + urlEncode(accountId);
1080 std::string response = curlGet(ssoUrl,
"x-amz-sso_bearer_token: " + accessToken);
1081 if(response.empty())
1083 grklog.
warn(
"S3Fetcher: SSO GetRoleCredentials failed");
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");
1093 if(ak.empty() || sk.empty() || st.empty())
1095 grklog.
warn(
"S3Fetcher: SSO returned incomplete credentials");
1099 auth_.username_ = ak;
1100 auth_.password_ = sk;
1101 auth_.session_token_ = st;
1102 resolveRegion(getProfile());
1105 if(!expirationMs.empty())
1109 expTime = std::stoll(expirationMs) / 1000;
1115 cacheCredentials(AWSCredentialSource::SSO, ak, sk, st, expTime);
1118 std::lock_guard<std::mutex> lock(c.mutex);
1119 c.ssoStartURL = startUrl;
1120 c.ssoAccountId = accountId;
1121 c.ssoRoleName = roleName;
1124 grklog.
debug(
"S3Fetcher: obtained SSO credentials");
1130 bool tryCredentialProcess(
const std::string& command)
1135 grklog.
debug(
"S3Fetcher: executing credential_process: %s", command.c_str());
1137 FILE* pipe = popen(command.c_str(),
"r");
1140 grklog.
warn(
"S3Fetcher: failed to execute credential_process: %s", command.c_str());
1146 while(fgets(buffer,
sizeof(buffer), pipe))
1149 int exitCode = pclose(pipe);
1152 grklog.
warn(
"S3Fetcher: credential_process exited with code %d", exitCode);
1158 grklog.
warn(
"S3Fetcher: credential_process returned empty output");
1162 std::string version = extractJsonString(output,
"Version");
1165 grklog.
warn(
"S3Fetcher: credential_process Version '%s' unsupported (expected '1')",
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");
1175 if(ak.empty() || sk.empty())
1178 "S3Fetcher: credential_process did not return required AccessKeyId/SecretAccessKey");
1182 auth_.username_ = ak;
1183 auth_.password_ = sk;
1184 auth_.session_token_ = st;
1185 resolveRegion(getProfile());
1187 cacheCredentials(AWSCredentialSource::CREDENTIAL_PROCESS, ak, sk, st,
1188 expiration.empty() ? 0 : parseIso8601(expiration));
1191 std::lock_guard<std::mutex> lock(c.mutex);
1192 c.credentialProcess = command;
1195 grklog.
debug(
"S3Fetcher: obtained credentials from credential_process");
1203 bool useHttps()
const
1205 if(auth_.s3_use_https_ != 0)
1206 return auth_.s3_use_https_ > 0;
1207 return EnvVarManager::get_string(
"AWS_HTTPS",
"YES") !=
"NO";
1210 static std::string formatPort(
int port,
bool https)
1212 int defaultPort = https ? 443 : 80;
1213 return port != defaultPort ?
":" + std::to_string(port) :
"";
1216 bool isVirtualHostingEnabled()
const
1218 if(auth_.s3_use_virtual_hosting_ != 0)
1219 return auth_.s3_use_virtual_hosting_ > 0;
1220 return EnvVarManager::test_bool(
"AWS_VIRTUAL_HOSTING");
1223 void configureEndpoint(ParsedFetchPath& parsed,
bool useVirtualHosting)
1225 bool https = useHttps();
1226 int defaultPort = https ? 443 : 80;
1229 std::optional<std::string> s3Endpoint;
1230 if(!auth_.s3_endpoint_.empty())
1231 s3Endpoint = auth_.s3_endpoint_;
1233 s3Endpoint = EnvVarManager::get(
"AWS_S3_ENDPOINT");
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);
1243 size_t portPos = endpoint.find(
':');
1244 if(portPos != std::string::npos)
1246 parsed.host = endpoint.substr(0, portPos);
1249 parsed.port = std::stoi(endpoint.substr(portPos + 1));
1253 parsed.port = defaultPort;
1258 parsed.host = endpoint;
1259 parsed.port = defaultPort;
1262 if(useVirtualHosting)
1263 parsed.host = parsed.bucket +
"." + parsed.host;
1267 parsed.host = useVirtualHosting ? parsed.bucket +
".s3." + auth_.region_ +
".amazonaws.com"
1268 :
"s3." + auth_.region_ +
".amazonaws.com";
1269 parsed.port = defaultPort;
1273 bool handleVirtualHosting(
const std::string& url, ParsedFetchPath& parsed)
1275 if(!isVirtualHostingEnabled())
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);
1286 size_t slashPos = urlBody.find(
'/');
1287 if(slashPos == std::string::npos)
1290 std::string hostPort = urlBody.substr(0, slashPos);
1291 std::string pathPart = urlBody.substr(slashPos + 1);
1294 std::string host = hostPort;
1295 size_t colonPos = hostPort.find(
':');
1296 if(colonPos != std::string::npos)
1298 host = hostPort.substr(0, colonPos);
1301 parsed.port = std::stoi(hostPort.substr(colonPos + 1));
1305 parsed.port = useHttps() ? 443 : 80;
1310 std::string s3Endpoint;
1311 if(!auth_.s3_endpoint_.empty())
1312 s3Endpoint = auth_.s3_endpoint_;
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);
1325 if(host.ends_with(s3Endpoint))
1327 size_t bucketLen = host.length() - s3Endpoint.length() - 1;
1328 if(bucketLen > 0 && host[bucketLen] ==
'.')
1330 parsed.bucket = host.substr(0, bucketLen);
1332 parsed.key = pathPart;
1333 grklog.
debug(
"S3Fetcher: detected virtual-hosted URL: bucket=%s", parsed.bucket.c_str());
1341 static void parseHttpUrl(std::string& url, ParsedFetchPath& parsed)
1343 if(!url.starts_with(
"http://"))
1344 throw std::runtime_error(
"Invalid HTTP URL");
1345 url = url.substr(7);
1347 size_t slashPos = url.find(
'/');
1348 if(slashPos == std::string::npos)
1349 throw std::runtime_error(
"Invalid HTTP URL: no path");
1351 std::string hostPort = url.substr(0, slashPos);
1352 std::string path = url.substr(slashPos + 1);
1354 size_t colonPos = hostPort.find(
':');
1355 if(colonPos != std::string::npos)
1357 parsed.host = hostPort.substr(0, colonPos);
1360 parsed.port = std::stoi(hostPort.substr(colonPos + 1));
1369 parsed.host = hostPort;
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);
1384 void resolveRegion(
const std::string& profile)
1386 if(
auto r = EnvVarManager::get(
"AWS_REGION"))
1391 if(
auto r = EnvVarManager::get(
"AWS_DEFAULT_REGION"))
1398 if(auth_.region_.empty())
1400 std::string configFile = getConfigFilePath();
1401 if(!configFile.empty())
1404 if(parser.parse(configFile))
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())
1411 auto regionIt = it->second.find(
"region");
1412 if(regionIt != it->second.end() && !regionIt->second.empty())
1414 auth_.region_ = regionIt->second;
1422 if(auth_.region_.empty())
1423 auth_.region_ =
"us-east-1";
1426 static std::string getProfile()
1428 if(
auto p = EnvVarManager::get(
"AWS_PROFILE"))
1430 if(
auto p = EnvVarManager::get(
"AWS_DEFAULT_PROFILE"))
1435 static std::string getConfigFilePath()
1437 if(
auto p = EnvVarManager::get(
"AWS_CONFIG_FILE"))
1440 if(
auto p = EnvVarManager::get(
"USERPROFILE"))
1441 return *p +
"\\.aws\\config";
1443 if(
auto p = EnvVarManager::get(
"HOME"))
1444 return *p +
"/.aws/config";
1449 static std::string getCredentialsFilePath()
1451 if(
auto p = EnvVarManager::get(
"GRK_AWS_CREDENTIALS_FILE"))
1454 if(
auto p = EnvVarManager::get(
"USERPROFILE"))
1455 return *p +
"\\.aws\\credentials";
1457 if(
auto p = EnvVarManager::get(
"HOME"))
1458 return *p +
"/.aws/credentials";
1463 static std::string getAWSRootDir()
1465 if(
auto p = EnvVarManager::get(
"GRK_AWS_ROOT_DIR"))
1468 if(
auto p = EnvVarManager::get(
"USERPROFILE"))
1469 return *p +
"\\.aws";
1471 if(
auto p = EnvVarManager::get(
"HOME"))
1472 return *p +
"/.aws";
1477 std::string buildSTSUrl()
1479 std::string regional = EnvVarManager::get_string(
"AWS_STS_REGIONAL_ENDPOINTS",
"regional");
1481 if(regional ==
"regional")
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";
1489 stsUrl =
"https://sts.amazonaws.com";
1491 if(
auto endpoint = EnvVarManager::get(
"GRK_AWS_STS_ROOT_URL"))
1496 void cacheCredentials(AWSCredentialSource source,
const std::string& ak,
const std::string& sk,
1497 const std::string& st, time_t expiration)
1500 std::lock_guard<std::mutex> lock(c.mutex);
1504 c.sessionToken = st;
1505 c.region = auth_.region_;
1506 c.expiration = expiration;
1511 void applyInsecureSSL(CURL* curl)
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"))
1518 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
1519 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
1523 std::string curlGet(
const std::string& url,
const std::string& header =
"")
1525 CURL* curl = curl_easy_init();
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);
1536 struct curl_slist* headers =
nullptr;
1539 headers = curl_slist_append(headers, header.c_str());
1540 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
1543 CURLcode res = curl_easy_perform(curl);
1545 curl_slist_free_all(headers);
1546 curl_easy_cleanup(curl);
1548 return res == CURLE_OK ? response : std::string{};
1551 std::string curlGetWithToken(
const std::string& url,
const std::string& token,
long timeout = 5)
1553 CURL* curl = curl_easy_init();
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);
1564 struct curl_slist* headers =
nullptr;
1567 headers = curl_slist_append(headers, (
"X-aws-ec2-metadata-token: " + token).c_str());
1568 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
1571 CURLcode res = curl_easy_perform(curl);
1573 curl_slist_free_all(headers);
1574 curl_easy_cleanup(curl);
1576 return res == CURLE_OK ? response : std::string{};
1581 static std::string extractXmlValue(
const std::string& xml,
const std::string& tag)
1583 std::string openTag =
"<" + tag +
">";
1584 std::string closeTag =
"</" + tag +
">";
1585 size_t pos = xml.find(openTag);
1586 if(pos == std::string::npos)
1588 pos += openTag.length();
1589 size_t end = xml.find(closeTag, pos);
1590 if(end == std::string::npos)
1592 return xml.substr(pos, end - pos);
1595 static std::string extractJsonString(
const std::string& json,
const std::string& key)
1597 std::string pattern =
"\"" + key +
"\"";
1598 size_t pos = json.find(pattern);
1599 if(pos == std::string::npos)
1601 pos += pattern.length();
1604 while(pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t' || json[pos] ==
':'))
1607 if(pos >= json.size())
1610 if(json[pos] ==
'"')
1615 while(end < json.size() && json[end] !=
'"')
1617 if(json[end] ==
'\\')
1621 return json.substr(pos, end - pos);
1626 while(end < json.size() && json[end] !=
',' && json[end] !=
'}' && json[end] !=
' ' &&
1629 return json.substr(pos, end - pos);
1632 static bool readFileContents(
const std::string& path, std::string& contents)
1634 std::ifstream file(path, std::ios::binary);
1637 contents.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
1641 static void trimWhitespace(std::string& s)
1643 s.erase(0, s.find_first_not_of(
" \t\r\n"));
1644 s.erase(s.find_last_not_of(
" \t\r\n") + 1);
1647 static time_t parseIso8601(
const std::string& str)
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)
1659 return _mkgmtime(&tm);
1667 static std::string urlEncode(
const std::string& str)
1669 CURL* curl = curl_easy_init();
1672 char* encoded = curl_easy_escape(curl, str.c_str(), (
int)str.length());
1673 std::string result(encoded);
1675 curl_easy_cleanup(curl);
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