21#include "XrdVersion.hh"
27#include <openssl/evp.h>
28#include <openssl/hmac.h>
38std::shared_mutex Factory::m_bucket_auth_map_mutex;
39bool Factory::m_initialized =
false;
41std::once_flag Factory::m_init_once;
42std::string Factory::m_endpoint =
"";
43std::string Factory::m_service =
"s3";
44std::string Factory::m_region =
"";
45std::string Factory::m_url_style =
"path";
46std::string Factory::m_mkdir_sentinel;
47Factory::Credentials Factory::m_default_creds;
48std::unordered_map<std::string, Factory::Credentials> Factory::m_bucket_location_map;
49std::unordered_map<std::string, std::pair<Factory::Credentials, std::chrono::steady_clock::time_point>> Factory::m_bucket_auth_map;
55AmazonURLEncode(
const std::string &input) {
62 output.reserve(input.size());
63 for (
const auto & val : input) {
69 if ((
'A' <= val && val <=
'Z') ||
70 (
'a' <= val && val <=
'z') ||
71 (
'0' <= val && val <=
'9') || val ==
'-' ||
72 val ==
'_' || val ==
'.' || val ==
'~') {
73 output.append(1, val);
75 char percentEncode[4];
76 snprintf(percentEncode, 4,
"%%%.2hhX", val);
77 output.append(percentEncode);
86 std::call_once(m_init_once, [&] {
103Factory::CanonicalizeQueryString(
const std::string &url) {
104 auto loc = url.find(
"://");
105 if (loc == std::string::npos) {
109 loc = url.find(
'?', loc);
110 if (loc == std::string::npos) {
113 std::vector<std::pair<std::string, std::string>> query_parameters;
114 auto param_end = url.find(
'&', loc);
115 while (loc != std::string::npos) {
116 auto param_start = loc + 1;
117 loc = url.find(
'=', param_start);
118 if (loc == param_start) {
121 else if (loc >= param_end) {
122 auto param = url.substr(param_start, param_end - param_start);
123 if (!param.empty()) {
125 query_parameters.emplace_back(AmazonURLEncode(param),
"");
128 std::string name = url.substr(param_start, loc - param_start);
130 auto value_start = loc;
132 if (param_end == std::string::npos) {
133 value = url.substr(value_start);
135 value = url.substr(value_start, param_end - value_start);
137 if (!value.empty()) {
138 query_parameters.emplace_back(AmazonURLEncode(name), AmazonURLEncode(value));
142 if (loc != std::string::npos) {
143 param_end = url.find(
'&', loc + 1);
146 std::sort(query_parameters.begin(), query_parameters.end(),
147 [](
const auto &a,
const auto &b) { return a.first < b.first; });
149 size_t string_size = 0;
150 for (
const auto ¶m : query_parameters) {
151 string_size += param.first.size() + param.second.size() + 2;
153 std::string canonicalQueryString;
155 canonicalQueryString.reserve(string_size);
157 for (
const auto ¶m : query_parameters) {
160 canonicalQueryString += param.first +
'=' + param.second;
163 canonicalQueryString +=
'&';
166 if (!canonicalQueryString.empty()) {
167 canonicalQueryString.erase(canonicalQueryString.end() - 1);
169 return canonicalQueryString;
174 if (!m_initialized) {
return nullptr;}
175 return new File(m_log);
180 if (!m_initialized) {
return nullptr;}
186void SetDefault(
XrdCl::Env *env,
const std::string &optName,
const std::string &envName, std::string &location,
const std::string &def) {
188 if (!env->
GetString(optName, val) || val.empty()) {
192 if (env->
GetString(optName, val) && !val.empty()) {
200std::string_view
ltrim_view(
const std::string_view input_view) {
201 for (
size_t idx = 0; idx < input_view.size(); idx++) {
202 if (!isspace(input_view[idx])) {
203 return input_view.substr(idx);
209bool ComputeSHA256(
const std::string_view payload, std::vector<unsigned char> &messageDigest) {
210 EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
215 if (!EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) {
216 EVP_MD_CTX_destroy(mdctx);
220 if (!EVP_DigestUpdate(mdctx, payload.data(), payload.length())) {
221 EVP_MD_CTX_destroy(mdctx);
225 unsigned int mdLength;
226 if (!EVP_DigestFinal_ex(mdctx, messageDigest.data(), &mdLength)) {
227 EVP_MD_CTX_destroy(mdctx);
230 messageDigest.resize(mdLength);
232 EVP_MD_CTX_destroy(mdctx);
236void MessageDigestAsHex(
const std::vector<unsigned char> messageDigest,
237 std::string &hexEncoded) {
238 hexEncoded.resize(messageDigest.size() * 2);
239 char *ptr = hexEncoded.data();
240 for (
unsigned int idx = 0; idx < messageDigest.size(); ++idx, ptr += 2) {
241 snprintf(ptr, 3,
"%02x", messageDigest[idx]);
248ssize_t FullRead(
int fd,
void *ptr,
size_t nbytes) {
249 ssize_t nleft, nread;
254 nread =
read(fd, ptr, nleft);
256 if (errno == EINTR) {
260 }
else if (nread == 0) {
264 ptr =
static_cast<char *
>(ptr) + nread;
266 return (nbytes - nleft);
272ReadShortFile(
const std::string &fileName, std::string &contents, std::string &err_msg) {
273 int fd =
open(fileName.c_str(), O_RDONLY, 0600);
275 err_msg =
"Failed to open file '" + fileName +
"': " + std::string(strerror(errno));
278 contents.resize(32*1024);
280 auto totalRead = FullRead(fd, contents.data(), contents.size());
282 if (totalRead == -1) {
283 err_msg =
"Failed to read file '" + fileName +
"': " + std::string(strerror(errno));
286 contents.resize(totalRead);
294 std::string obj = input_obj;
295 auto loc = input_obj.find(
'?');
296 if (loc != std::string::npos) {
297 auto query = std::string_view(input_obj).substr(loc + 1);
298 obj = obj.substr(0, loc);
299 bool added_query =
false;
300 while (!query.empty()) {
301 auto next_query_loc = query.find(
'&');
302 auto current_query = (next_query_loc == std::string::npos) ? query : query.substr(0, next_query_loc);
303 query = (next_query_loc == std::string::npos) ?
"" : query.substr(next_query_loc + 1);
304 if (current_query.empty()) {
307 auto equal_loc = current_query.find(
'=');
308 if (equal_loc != std::string::npos) {
309 auto key = current_query.substr(0, equal_loc);
310 if (key !=
"authz") {
311 obj += (added_query ?
"&" :
"?") + std::string(current_query);
314 }
else if (current_query !=
"authz") {
315 obj += (added_query ?
"&" :
"?") + std::string(current_query);
327 auto loc = url.find(
"://");
328 if (loc == std::string_view::npos) {
332 auto slash_loc = url.find(
'/', loc);
333 auto query_loc = url.find(
'?', loc);
334 if (query_loc != std::string_view::npos && (slash_loc == std::string_view::npos || query_loc < slash_loc)) {
335 slash_loc = query_loc;
337 auto authority = url.substr(loc, slash_loc - loc);
338 if (authority.empty()) {
341 auto at_loc = authority.find(
'@');
342 if (at_loc != std::string_view::npos) {
344 authority = authority.substr(at_loc + 1);
347 auto colon_loc = authority.find(
':');
348 if (colon_loc != std::string_view::npos) {
349 authority = authority.substr(0, colon_loc);
355Factory::InitS3Config()
358 SetDefault(env,
"XrdClS3MkdirSentinel",
"XRDCLS3_MKDIRSENTINEL", m_mkdir_sentinel,
".xrdcls3.dirsentinel");
359 SetDefault(env,
"XrdClS3Endpoint",
"XRDCLS3_ENDPOINT", m_endpoint,
"");
360 SetDefault(env,
"XrdClS3UrlStyle",
"XRDCLS3_URLSTYLE", m_url_style,
"path");
361 SetDefault(env,
"XrdClS3Region",
"XRDCLS3_REGION", m_region,
"");
362 std::string access_key;
363 SetDefault(env,
"XrdClS3AccessKeyLocation",
"XRDCLS3_ACCESSKEYLOCATION", access_key,
"");
364 std::string secret_key;
365 SetDefault(env,
"XrdClS3SecretKeyLocation",
"XRDCLS3_SECRETKEYLOCATION", secret_key,
"");
366 if (!access_key.empty() && !secret_key.empty()) {
367 m_default_creds = {access_key, secret_key};
368 }
else if (access_key.empty() && secret_key.empty()) {
369 m_log->Info(
kLogXrdClS3,
"Defaulting to public bucket access");
370 }
else if (access_key.empty() && !secret_key.empty()) {
371 m_log->Warning(
kLogXrdClS3,
"Secret key location set (%s) but access key location is empty; authorization will not work.", secret_key.c_str());
372 }
else if (!access_key.empty() && secret_key.empty()) {
373 m_log->Warning(
kLogXrdClS3,
"Access key location set (%s) but secret key location is empty; authorization will not work.", access_key.c_str());
377 std::string bucket_configs;
378 SetDefault(env,
"XrdClS3BucketConfigs",
"XRDCLS3_BUCKETCONFIGS", bucket_configs,
"");
379 if (!bucket_configs.empty()) {
380 std::stringstream ss(bucket_configs);
381 std::string config_name;
382 while (std::getline(ss, config_name)) {
383 auto name = TrimView(config_name);
384 auto bucket_name_key = std::string(
"XrdClS3") + std::string(name) +
"BucketName";
385 std::string bucket_name_val;
386 if (!env->
GetString(bucket_name_key, bucket_name_val) || bucket_name_val.empty()) {
387 m_log->Warning(
kLogXrdClS3,
"Per-bucket config includes entry '%s' but XrdClS3%sBucketName is not set", std::string(name).c_str(), std::string(name).c_str());
390 auto access_key_location_key = std::string(
"XrdClS3") + std::string(name) +
"AccessKeyLocation";
391 std::string access_key_location_val;
392 auto has_access_key = env->
GetString(access_key_location_key, access_key_location_val) && !access_key_location_val.empty();
394 auto secret_key_location_key = std::string(
"XrdClS3") + std::string(name) +
"SecretKeyLocation";
395 std::string secret_key_location_val;
396 auto has_secret_key = env->
GetString(secret_key_location_key, secret_key_location_val) && !secret_key_location_val.empty();
398 if (has_access_key && has_secret_key) {
399 m_bucket_location_map[bucket_name_val] = {access_key_location_val, secret_key_location_val};
400 }
else if (!has_access_key && !has_secret_key) {
402 m_bucket_location_map[bucket_name_val] = {
"",
""};
403 }
else if (has_access_key && !has_secret_key) {
404 m_log->Warning(
kLogXrdClS3,
"Per-bucket config for entry '%s' has an access key location set (%s) but no secret key", std::string(name).c_str(), access_key_location_val.c_str());
406 m_log->Warning(
kLogXrdClS3,
"Per-bucket config for entry '%s' has an secret key location set (%s) but no access key", std::string(name).c_str(), secret_key_location_val.c_str());
414 if (s3_url.substr(0, 5) !=
"s3://") {
415 err_msg =
"Provided URL does not start with s3://";
418 auto loc = s3_url.find(
'/', 5);
419 auto bucket = s3_url.substr(5, loc - 5);
420 auto at_loc = bucket.find(
'@');
421 if (at_loc != std::string::npos) {
422 std::string login =
"";
423 login = bucket.substr(0, at_loc);
424 bucket = bucket.substr(at_loc + 1);
426 std::string endpoint = m_endpoint;
427 std::string region = m_region;
428 if ((bucket == m_endpoint) || m_endpoint.empty()) {
430 auto old_loc = loc + 1;
431 loc = s3_url.find(
'/', loc + 1);
432 if (loc == std::string::npos) {
433 err_msg =
"Provided S3 URL does not contain a bucket in path";
436 bucket = s3_url.substr(old_loc, loc - old_loc);
439 std::string test_endpoint =
"." + endpoint;
440 if (!m_region.empty()) {
441 auto bucket_loc = authority.rfind(
"." + m_region + test_endpoint);
442 if (bucket_loc != std::string::npos) {
443 bucket = authority.substr(0, bucket_loc);
445 auto bucket_loc = authority.rfind(test_endpoint);
446 if (bucket_loc != std::string::npos) {
447 bucket = authority.substr(0, bucket_loc);
451 auto bucket_loc = authority.rfind(test_endpoint);
452 if (bucket_loc != std::string::npos) {
453 bucket = authority.substr(0, bucket_loc);
458 if (loc != std::string::npos) {
459 obj = s3_url.substr(loc + 1);
466 if (m_url_style ==
"virtual" || m_url_style.empty()) {
467 https_url =
"https://" + bucket +
"." + m_region +
"." + endpoint + (obj_result ?
"" : (
"/" + obj));
469 }
else if (m_url_style ==
"path") {
470 if (!m_region.empty()) {
471 https_url =
"https://" + m_region +
"." + endpoint +
"/" + bucket + (obj_result ?
"" : (
"/" + obj));
473 https_url =
"https://" + endpoint +
"/" + bucket + (obj_result ?
"" : (
"/" + obj));
477 err_msg =
"Server configuration has invalid setting for URL style";
483Factory::GenerateV4Signature(
const std::string &url,
const std::string &verb, std::vector<std::pair<std::string, std::string>> &headers, std::string &auth_token, std::string &err_msg) {
494 if (secretKey.empty()) {
507 auto canonicalQueryString = CanonicalizeQueryString(url);
511 if (std::find_if(headers.begin(), headers.end(),
512 [](
const auto &pair) { return pair.first ==
"Host"; }) == headers.end()) {
515 err_msg =
"Unable to extract hostname from URL: " + url;
518 headers.emplace_back(
"Host", host);
522 auto iter = std::find_if(headers.begin(), headers.end(),
523 [](
const auto &pair) { return !strcasecmp(pair.first.c_str(),
"X-Amz-Date"); });
524 std::string date_time;
525 char date_char[] =
"YYYYMMDD";
526 if (iter == headers.end()) {
529 struct tm brokenDownTime;
530 gmtime_r(&now, &brokenDownTime);
532 date_time =
"YYYYMMDDThhmmssZ";
533 strftime(date_time.data(), date_time.size(),
"%Y%m%dT%H%M%SZ", &brokenDownTime);
534 headers.emplace_back(
"X-Amz-Date", date_time);
535 strftime(date_char,
sizeof(date_char),
"%Y%m%d", &brokenDownTime);
537 date_time = iter->second;
538 auto loc = date_time.find(
'T', 0);
540 err_msg =
"Invalid value for X-Amz-Date";
543 memcpy(date_char, date_time.c_str(), 8);
549 std::string payload_hash =
"UNSIGNED-PAYLOAD";
550 iter = std::find_if(headers.begin(), headers.end(),
551 [](
const auto &pair) { return !strcasecmp(pair.first.c_str(),
"X-Amz-Content-Sha256"); });
552 if (iter == headers.end()) {
553 headers.emplace_back(
"X-Amz-Content-Sha256", payload_hash);
555 payload_hash = iter->second;
561 std::vector<std::pair<std::string, std::string>> transformed_headers;
562 transformed_headers.reserve(headers.size());
563 for (
const auto &info : headers) {
564 std::string header = info.first;
565 std::transform(header.begin(), header.end(), header.begin(), &tolower);
567 std::string value = info.second;
571 auto value_trimmed = std::string(
TrimView(value));
576 bool inSpaces =
false;
577 while (right < value_trimmed.length()) {
579 if (value_trimmed[right] ==
' ') {
587 if (value_trimmed[right] ==
' ') {
591 value_trimmed.erase(left, right - left - 1);
597 transformed_headers.emplace_back(header, value);
599 std::sort(transformed_headers.begin(), transformed_headers.end(),
600 [](
const auto &a,
const auto &b) { return a.first < b.first; });
604 std::string signedHeaders, canonicalHeaders;
605 for (
const auto &info : transformed_headers) {
606 canonicalHeaders += info.first +
":" + info.second +
"\n";
607 signedHeaders += info.first +
";";
609 signedHeaders.erase(signedHeaders.end() - 1);
612 auto canonicalRequest =
613 verb +
"\n" + canonicalURI +
"\n" + canonicalQueryString +
"\n" +
614 canonicalHeaders +
"\n" + signedHeaders +
"\n" + payload_hash;
621 std::string canonicalRequestHash;
622 std::vector<unsigned char> messageDigest;
623 messageDigest.resize(EVP_MAX_MD_SIZE);
624 if (!ComputeSHA256(canonicalRequest, messageDigest)) {
625 err_msg =
"Unable to hash canonical request.";
628 MessageDigestAsHex(messageDigest, canonicalRequestHash);
631 auto credentialScope = std::string(date_char) +
"/" + m_region +
"/" + m_service +
"/aws4_request";
632 auto stringToSign = std::string(
"AWS4-HMAC-SHA256\n") + date_time +
"\n" + credentialScope +
"\n" + canonicalRequestHash;
640 auto saKey = std::string(
"AWS4") + secretKey;
641 unsigned int mdLength = 0;
642 const unsigned char *hmac =
643 HMAC(EVP_sha256(), saKey.c_str(), saKey.length(), (
unsigned char *)date_char,
644 sizeof(date_char) - 1, messageDigest.data(), &mdLength);
646 err_msg =
"Unable to calculate HMAC for date.";
650 unsigned int md2Length = 0;
651 unsigned char messageDigest2[EVP_MAX_MD_SIZE];
652 hmac = HMAC(EVP_sha256(), messageDigest.data(), mdLength,
653 reinterpret_cast<unsigned char *
>(m_region.data()), m_region.size(), messageDigest2,
656 err_msg =
"Unable to calculate HMAC for region.";
660 hmac = HMAC(EVP_sha256(), messageDigest2, md2Length,
661 reinterpret_cast<unsigned char *
>(m_service.data()), m_service.size(), messageDigest.data(),
664 err_msg =
"Unable to calculate HMAC for service.";
668 const char request_char[] =
"aws4_request";
669 hmac = HMAC(EVP_sha256(), messageDigest.data(), messageDigest.size(),
reinterpret_cast<const unsigned char *
>(request_char),
670 sizeof(request_char) - 1, messageDigest2, &md2Length);
672 err_msg =
"Unable to calculate HMAC for request.";
676 hmac = HMAC(EVP_sha256(), messageDigest2, md2Length,
677 reinterpret_cast<unsigned char *
>(stringToSign.data()),
678 stringToSign.size(), messageDigest.data(), &mdLength);
680 err_msg =
"Unable to calculate HMAC for request string.";
684 std::string signature;
685 MessageDigestAsHex(messageDigest, signature);
688 std::string(
"AWS4-HMAC-SHA256 Credential=") + keyId +
"/" + credentialScope +
689 ",SignedHeaders=" + signedHeaders +
",Signature=" + signature;
695 if (m_url_style ==
"virtual" || m_url_style.empty()) {
698 if (hostname.empty()) {
701 auto test_endpoint =
"." + m_endpoint;
702 if (!m_region.empty()) test_endpoint =
"." + m_region + test_endpoint;
703 auto loc = hostname.rfind(test_endpoint);
704 if (loc == std::string::npos) {
705 if (!m_region.empty()) {
706 loc = hostname.rfind(
"." + m_endpoint);
707 if (loc != std::string::npos) {
708 return std::string(hostname.substr(0, loc));
713 return std::string(hostname.substr(0, loc));
714 }
else if (m_url_style ==
"path") {
716 auto loc = url.find(
"://");
717 if (loc == std::string::npos) {
721 auto slash_loc = url.find(
'/', loc);
722 if (slash_loc == std::string::npos) {
725 auto bucket_start = slash_loc + 1;
726 auto bucket_end = url.find(
'/', bucket_start);
727 if (bucket_end == std::string::npos) {
728 return url.substr(bucket_start);
730 return url.substr(bucket_start, bucket_end - bucket_start);
737std::tuple<std::string, std::string, bool>
740 auto now = std::chrono::steady_clock::now();
742 std::shared_lock lock(m_bucket_auth_map_mutex);
743 auto iter = m_bucket_auth_map.find(bucket);
744 if (iter != m_bucket_auth_map.end()) {
746 auto &creds = iter->second.first;
747 auto &expiration = iter->second.second;
748 if (now < expiration) {
750 return {creds.m_accesskey, creds.m_secretkey,
true};
755 std::unique_lock lock(m_bucket_auth_map_mutex);
756 auto iter = m_bucket_location_map.find(bucket);
757 std::string access_key_location, secret_key_location;
758 if (iter == m_bucket_location_map.end()) {
760 if (m_default_creds.m_accesskey.empty() || m_default_creds.m_secretkey.empty()) {
762 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::minutes(1)};
763 return {
"",
"",
true};
765 access_key_location = m_default_creds.m_accesskey;
766 secret_key_location = m_default_creds.m_secretkey;
768 access_key_location = iter->second.m_accesskey;
769 secret_key_location = iter->second.m_secretkey;
771 if (access_key_location.empty() && secret_key_location.empty()) {
773 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::minutes(1)};
774 return {
"",
"",
true};
776 if (access_key_location.empty() || secret_key_location.empty()) {
777 err_msg =
"No credentials available for bucket: " + bucket;
778 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
779 return {
"",
"",
false};
782 std::string access_key, secret_key;
783 if (!ReadShortFile(access_key_location, access_key, err_msg)) {
784 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
785 return {
"",
"",
false};
789 if (!ReadShortFile(secret_key_location, secret_key, err_msg)) {
790 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
791 return {
"",
"",
false};
795 if (access_key.empty() || secret_key.empty()) {
796 err_msg =
"Credentials for bucket '" + bucket +
"' are empty.";
797 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
798 return {
"",
"",
false};
800 m_bucket_auth_map[bucket] = {{access_key, secret_key}, now + std::chrono::minutes(1)};
801 return {access_key, secret_key,
true};
806 auto loc = url.find(
"://");
807 if (loc == std::string_view::npos) {
810 auto path_loc = url.find(
"/", loc + 3);
811 auto query_loc = url.find(
"?", loc + 3);
812 if (query_loc != std::string_view::npos && (path_loc == std::string_view::npos || query_loc < path_loc)) {
816 auto path = url.substr(path_loc, query_loc - path_loc);
822 const auto length = path.size();
823 while (offset < length) {
824 next = strcspn(path.data() + offset,
"/");
830 if (offset + next >= length) {
831 next = length - offset;
834 segment = std::string(path.data() + offset, next);
835 encoded += AmazonURLEncode(segment);
845 auto view = ltrim_view(input_view);
846 for (
size_t idx = 0; idx < input_view.size(); idx++) {
847 if (!isspace(view[view.size() - 1 - idx])) {
848 return view.substr(0, view.size() - idx);
858 return static_cast<void*
>(
new Factory());
XrdVERSIONINFO(XrdClGetPlugIn, XrdClGetPlugIn) extern "C"
XrdEXPORT void * XrdClGetPlugIn(const void *)
virtual XrdCl::FilePlugIn * CreateFile(const std::string &url) override
Create a file plug-in for the given URL.
static std::string_view ExtractHostname(const std::string_view url)
static std::string PathEncode(const std::string_view url)
static std::string CleanObjectName(const std::string &object)
static bool GenerateHttpUrl(const std::string &s3_url, std::string &https_url, std::string *obj_result, std::string &err_msg)
virtual XrdCl::FileSystemPlugIn * CreateFileSystem(const std::string &url) override
Create a file system plug-in for the given URL.
static std::tuple< std::string, std::string, bool > GetCredentialsForBucket(const std::string &bucket, std::string &err_msg)
static bool GenerateV4Signature(const std::string &url, const std::string &verb, std::vector< std::pair< std::string, std::string > > &headers, std::string &auth_token, std::string &err_msg)
static std::string_view TrimView(const std::string_view str)
static std::string GetBucketFromHttpsUrl(const std::string &url)
static Log * GetLog()
Get default log.
static Env * GetEnv()
Get default client environment.
bool PutString(const std::string &key, const std::string &value)
bool ImportString(const std::string &key, const std::string &shellKey)
bool GetString(const std::string &key, std::string &value)
An interface for file plug-ins.
An interface for file plug-ins.
std::string_view ltrim_view(const std::string_view &input_view)
const uint64_t kLogXrdClS3