XRootD
XrdClS3Factory.cc
Go to the documentation of this file.
1 /******************************************************************************/
2 /* Copyright (C) 2025, Pelican Project, Morgridge Institute for Research */
3 /* */
4 /* This file is part of the XrdClS3 client plugin for XRootD. */
5 /* */
6 /* XRootD is free software: you can redistribute it and/or modify it under */
7 /* the terms of the GNU Lesser General Public License as published by the */
8 /* Free Software Foundation, either version 3 of the License, or (at your */
9 /* option) any later version. */
10 /* */
11 /* XRootD is distributed in the hope that it will be useful, but WITHOUT */
12 /* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
13 /* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
14 /* License for more details. */
15 /* */
16 /* The copyright holder's institutional names and contributor's names may not */
17 /* be used to endorse or promote products derived from this software without */
18 /* specific prior written permission of the institution or contributor. */
19 /******************************************************************************/
20 
21 #include "XrdVersion.hh"
22 
23 #include "XrdClS3Factory.hh"
24 #include "XrdClS3File.hh"
25 #include "XrdClS3Filesystem.hh"
26 
27 #include <openssl/evp.h>
28 #include <openssl/hmac.h>
29 #include <XrdCl/XrdClDefaultEnv.hh>
30 #include <XrdCl/XrdClLog.hh>
31 
32 #include <fcntl.h>
33 
35 
36 using namespace XrdClS3;
37 
38 std::shared_mutex Factory::m_bucket_auth_map_mutex;
39 bool Factory::m_initialized = false;
40 XrdCl::Log *Factory::m_log{nullptr};
41 std::once_flag Factory::m_init_once;
42 std::string Factory::m_endpoint = "";
43 std::string Factory::m_service = "s3";
44 std::string Factory::m_region = "";
45 std::string Factory::m_url_style = "path";
46 std::string Factory::m_mkdir_sentinel;
47 Factory::Credentials Factory::m_default_creds;
48 std::unordered_map<std::string, Factory::Credentials> Factory::m_bucket_location_map;
49 std::unordered_map<std::string, std::pair<Factory::Credentials, std::chrono::steady_clock::time_point>> Factory::m_bucket_auth_map;
50 
51 
52 namespace {
53 
54 std::string
55 AmazonURLEncode(const std::string &input) {
56  /*
57  * See
58  * http://docs.amazonwebservices.com/AWSEC2/2010-11-15/DeveloperGuide/using-query-api.html
59  *
60  */
61  std::string output;
62  output.reserve(input.size());
63  for (const auto & val : input) {
64  // "Do not URL encode ... A-Z, a-z, 0-9, hyphen ( - ),
65  // underscore ( _ ), period ( . ), and tilde ( ~ ). Percent
66  // encode all other characters with %XY, where X and Y are hex
67  // characters 0-9 and uppercase A-F. Percent encode extended
68  // UTF-8 characters in the form %XY%ZA..."
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);
74  } else {
75  char percentEncode[4];
76  snprintf(percentEncode, 4, "%%%.2hhX", val);
77  output.append(percentEncode);
78  }
79  }
80  return output;
81 }
82 
83 }
84 
86  std::call_once(m_init_once, [&] {
87  m_log = XrdCl::DefaultEnv::GetLog();
88  if (!m_log) {
89  return;
90  }
91  m_log->SetTopicName(kLogXrdClS3, "XrdClS3");
92 
93  auto env = XrdCl::DefaultEnv::GetEnv();
94  if (!env) {
95  return;
96  }
97  InitS3Config();
98  m_initialized = true;
99  });
100 }
101 
102 std::string
103 Factory::CanonicalizeQueryString(const std::string &url) {
104  auto loc = url.find("://");
105  if (loc == std::string::npos) {
106  return "";
107  }
108  loc += 3; // Skip the "://"
109  loc = url.find('?', loc);
110  if (loc == std::string::npos) {
111  return "";
112  }
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; // Skip the '?' / '&'
117  loc = url.find('=', param_start);
118  if (loc == param_start) {
119  // Empty parameter name, skip
120  }
121  else if (loc >= param_end) {
122  auto param = url.substr(param_start, param_end - param_start);
123  if (!param.empty()) {
124  // No '=' found, treat as a parameter without value
125  query_parameters.emplace_back(AmazonURLEncode(param), "");
126  }
127  } else {
128  std::string name = url.substr(param_start, loc - param_start);
129  loc++; // Move past '='
130  auto value_start = loc;
131  std::string value;
132  if (param_end == std::string::npos) {
133  value = url.substr(value_start);
134  } else {
135  value = url.substr(value_start, param_end - value_start);
136  }
137  if (!value.empty()) {
138  query_parameters.emplace_back(AmazonURLEncode(name), AmazonURLEncode(value));
139  }
140  }
141  loc = param_end;
142  if (loc != std::string::npos) {
143  param_end = url.find('&', loc + 1);
144  }
145  }
146  std::sort(query_parameters.begin(), query_parameters.end(),
147  [](const auto &a, const auto &b) { return a.first < b.first; });
148 
149  size_t string_size = 0;
150  for (const auto &param : query_parameters) {
151  string_size += param.first.size() + param.second.size() + 2;
152  }
153  std::string canonicalQueryString;
154  if (string_size) {
155  canonicalQueryString.reserve(string_size);
156  }
157  for (const auto &param : query_parameters) {
158 
159  // Step 1C: Separate parameter names from values with '='.
160  canonicalQueryString += param.first + '=' + param.second;
161 
162  // Step 1D: Separate name-value pairs with '&';
163  canonicalQueryString += '&';
164  }
165  // We'll always have a superflous trailing ampersand.
166  if (!canonicalQueryString.empty()) {
167  canonicalQueryString.erase(canonicalQueryString.end() - 1);
168  }
169  return canonicalQueryString;
170 }
171 
173 Factory::CreateFile(const std::string & /*url*/) {
174  if (!m_initialized) {return nullptr;}
175  return new File(m_log);
176 }
177 
179 Factory::CreateFileSystem(const std::string & url) {
180  if (!m_initialized) {return nullptr;}
181  return new Filesystem(url, m_log);
182 }
183 
184 namespace {
185 
186 void SetDefault(XrdCl::Env *env, const std::string &optName, const std::string &envName, std::string &location, const std::string &def) {
187  std::string val;
188  if (!env->GetString(optName, val) || val.empty()) {
189  env->PutString(optName, "");
190  env->ImportString(optName, envName);
191  }
192  if (env->GetString(optName, val) && !val.empty()) {
193  location = val;
194  } else {
195  location = def;
196  }
197 }
198 
199 // Trim the left side of a string_view for space
200 std::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);
204  }
205  }
206  return "";
207 }
208 
209 bool ComputeSHA256(const std::string_view payload, std::vector<unsigned char> &messageDigest) {
210  EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
211  if (mdctx == NULL) {
212  return false;
213  }
214 
215  if (!EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) {
216  EVP_MD_CTX_destroy(mdctx);
217  return false;
218  }
219 
220  if (!EVP_DigestUpdate(mdctx, payload.data(), payload.length())) {
221  EVP_MD_CTX_destroy(mdctx);
222  return false;
223  }
224 
225  unsigned int mdLength;
226  if (!EVP_DigestFinal_ex(mdctx, messageDigest.data(), &mdLength)) {
227  EVP_MD_CTX_destroy(mdctx);
228  return false;
229  }
230  messageDigest.resize(mdLength);
231 
232  EVP_MD_CTX_destroy(mdctx);
233  return true;
234 }
235 
236 void 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]);
242  }
243 }
244 
245 // Helper function to read a file descriptor until EOF or
246 // `nbytes` bytes have been read.
247 // Includes appropriate handling of EINTR.
248 ssize_t FullRead(int fd, void *ptr, size_t nbytes) {
249  ssize_t nleft, nread;
250 
251  nleft = nbytes;
252  while (nleft > 0) {
253  REISSUE_READ:
254  nread = read(fd, ptr, nleft);
255  if (nread < 0) {
256  if (errno == EINTR) {
257  goto REISSUE_READ;
258  }
259  return -1;
260  } else if (nread == 0) {
261  break;
262  }
263  nleft -= nread;
264  ptr = static_cast<char *>(ptr) + nread;
265  }
266  return (nbytes - nleft);
267 }
268 
269 // Read a file into a string.
270 // If the file is larger than 32k, it will return false.
271 bool
272 ReadShortFile(const std::string &fileName, std::string &contents, std::string &err_msg) {
273  int fd = open(fileName.c_str(), O_RDONLY, 0600);
274  if (fd < 0) {
275  err_msg = "Failed to open file '" + fileName + "': " + std::string(strerror(errno));
276  return false;
277  }
278  contents.resize(32*1024);
279 
280  auto totalRead = FullRead(fd, contents.data(), contents.size());
281  close(fd);
282  if (totalRead == -1) {
283  err_msg = "Failed to read file '" + fileName + "': " + std::string(strerror(errno));
284  return false;
285  }
286  contents.resize(totalRead);
287  return true;
288 }
289 
290 } // namespace
291 
292 std::string
293 Factory::CleanObjectName(const std::string & input_obj) {
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()) {
305  continue;
306  }
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);
312  added_query = true;
313  }
314  } else if (current_query != "authz") {
315  obj += (added_query ? "&" : "?") + std::string(current_query);
316  added_query = true;
317  }
318  }
319  } else {
320  obj = input_obj;
321  }
322  return obj;
323 }
324 
325 std::string_view
326 Factory::ExtractHostname(const std::string_view url) {
327  auto loc = url.find("://");
328  if (loc == std::string_view::npos) {
329  return {};
330  }
331  loc += 3; // Move past "://"
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; // If there's a query, we stop at it
336  }
337  auto authority = url.substr(loc, slash_loc - loc);
338  if (authority.empty()) {
339  return {};
340  }
341  auto at_loc = authority.find('@');
342  if (at_loc != std::string_view::npos) {
343  // If there's an '@', we have user info, so we skip it
344  authority = authority.substr(at_loc + 1);
345  }
346  // If the authority contains a port, we need to strip it
347  auto colon_loc = authority.find(':');
348  if (colon_loc != std::string_view::npos) {
349  authority = authority.substr(0, colon_loc);
350  }
351  return authority;
352 }
353 
354 void
355 Factory::InitS3Config()
356 {
357  auto env = XrdCl::DefaultEnv::GetEnv();
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());
374  }
375 
376  // Parse the per-bucket configuration of credentials.
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());
388  continue;
389  }
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();
393 
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();
397 
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) {
401  // If both are empty, then it is implicitly a public bucket.
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());
405  } else {
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());
407  }
408  }
409  }
410 }
411 
412 bool
413 Factory::GenerateHttpUrl(const std::string &s3_url, std::string &https_url, std::string *obj_result, std::string &err_msg) {
414  if (s3_url.substr(0, 5) != "s3://") {
415  err_msg = "Provided URL does not start with s3://";
416  return false;
417  }
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);
425  }
426  std::string endpoint = m_endpoint;
427  std::string region = m_region;
428  if ((bucket == m_endpoint) || m_endpoint.empty()) {
429  endpoint = bucket;
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";
434  return false;
435  }
436  bucket = s3_url.substr(old_loc, loc - old_loc);
437  } else {
438  auto authority = ExtractHostname(s3_url);
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);
444  } else {
445  auto bucket_loc = authority.rfind(test_endpoint);
446  if (bucket_loc != std::string::npos) {
447  bucket = authority.substr(0, bucket_loc);
448  }
449  }
450  } else {
451  auto bucket_loc = authority.rfind(test_endpoint);
452  if (bucket_loc != std::string::npos) {
453  bucket = authority.substr(0, bucket_loc);
454  }
455  }
456  }
457  std::string obj;
458  if (loc != std::string::npos) {
459  obj = s3_url.substr(loc + 1);
460  }
461  // Strip out "authz" query parameters; those are internal to XRootD.
462  obj = CleanObjectName(obj);
463  if (obj_result) {
464  *obj_result = obj;
465  }
466  if (m_url_style == "virtual" || m_url_style.empty()) {
467  https_url = "https://" + bucket + "." + m_region + "." + endpoint + (obj_result ? "" : ("/" + obj));
468  return true;
469  } else if (m_url_style == "path") {
470  if (!m_region.empty()) {
471  https_url = "https://" + m_region + "." + endpoint + "/" + bucket + (obj_result ? "" : ("/" + obj));
472  } else {
473  https_url = "https://" + endpoint + "/" + bucket + (obj_result ? "" : ("/" + obj));
474  }
475  return true;
476  } else {
477  err_msg = "Server configuration has invalid setting for URL style";
478  return false;
479  }
480 }
481 
482 bool
483 Factory::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) {
484  auto bucket = GetBucketFromHttpsUrl(url);
485 
486  // If we're using temporary credentials, we need to add the token
487  // header here as well. We set saKey and keyID here (well before
488  // necessary) since we'll get them for free when we get the token.
489  auto [keyId, secretKey, ok] = GetCredentialsForBucket(bucket, err_msg);
490  if (!ok) {
491  return false;
492  }
493 
494  if (secretKey.empty()) {
495  auth_token = "";
496  return true;
497  }
498 
499  //
500  // Create task 1's inputs.
501  //
502 
503  auto canonicalURI = PathEncode(url);
504 
505  // The canonical query string is the alphabetically sorted list of
506  // URI-encoded parameter names '=' values, separated by '&'s.
507  auto canonicalQueryString = CanonicalizeQueryString(url);
508 
509  // The canonical headers must include the Host header, so add that
510  // now if we don't have it.
511  if (std::find_if(headers.begin(), headers.end(),
512  [](const auto &pair) { return pair.first == "Host"; }) == headers.end()) {
513  auto host = ExtractHostname(url);
514  if (host.empty()) {
515  err_msg = "Unable to extract hostname from URL: " + url;
516  return false;
517  }
518  headers.emplace_back("Host", host);
519  }
520 
521  // S3 complains if x-amz-date isn't signed, so do this early.
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()) {
527  time_t now;
528  time(&now);
529  struct tm brokenDownTime;
530  gmtime_r(&now, &brokenDownTime);
531 
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);
536  } else {
537  date_time = iter->second;
538  auto loc = date_time.find('T', 0);
539  if (loc != 8) {
540  err_msg = "Invalid value for X-Amz-Date";
541  return false;
542  }
543  memcpy(date_char, date_time.c_str(), 8);
544  }
545 
546  // The canonical payload hash is the lowercase hexadecimal string of the
547  // (SHA256) hash value of the payload or "UNSIGNED-PAYLOAD" if
548  // we are not signing the payload.
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);
554  } else {
555  payload_hash = iter->second;
556  }
557 
558  // The canonical list of headers is a sorted list of lowercase header
559  // names paired via ':' with the trimmed header value, each pair
560  // terminated with a newline.
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);
566 
567  std::string value = info.second;
568  if (value.empty()) {
569  continue;
570  }
571  auto value_trimmed = std::string(TrimView(value));
572 
573  // Convert internal runs of spaces into single spaces.
574  unsigned left = 1;
575  unsigned right = 1;
576  bool inSpaces = false;
577  while (right < value_trimmed.length()) {
578  if (!inSpaces) {
579  if (value_trimmed[right] == ' ') {
580  inSpaces = true;
581  left = right;
582  ++right;
583  } else {
584  ++right;
585  }
586  } else {
587  if (value_trimmed[right] == ' ') {
588  ++right;
589  } else {
590  inSpaces = false;
591  value_trimmed.erase(left, right - left - 1);
592  right = left + 1;
593  }
594  }
595  }
596 
597  transformed_headers.emplace_back(header, value);
598  }
599  std::sort(transformed_headers.begin(), transformed_headers.end(),
600  [](const auto &a, const auto &b) { return a.first < b.first; });
601 
602  // The canonical list of signed headers is trivial to generate while
603  // generating the list of headers.
604  std::string signedHeaders, canonicalHeaders;
605  for (const auto &info : transformed_headers) {
606  canonicalHeaders += info.first + ":" + info.second + "\n";
607  signedHeaders += info.first + ";";
608  }
609  signedHeaders.erase(signedHeaders.end() - 1);
610 
611  // Task 1: create the canonical request.
612  auto canonicalRequest =
613  verb + "\n" + canonicalURI + "\n" + canonicalQueryString + "\n" +
614  canonicalHeaders + "\n" + signedHeaders + "\n" + payload_hash;
615 
616  //
617  // Create task 2's inputs.
618  //
619 
620  // Hash the canonical request the way we did the payload.
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.";
626  return false;
627  }
628  MessageDigestAsHex(messageDigest, canonicalRequestHash);
629 
630  // Task 2: create the string to sign.
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;
633 
634  //
635  // Creating task 3's inputs was done when we checked to see if we needed
636  // to get the security token, since they come along for free when we do.
637  //
638 
639  // Task 3: calculate the signature.
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);
645  if (hmac == NULL) {
646  err_msg = "Unable to calculate HMAC for date.";
647  return false;
648  }
649 
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,
654  &md2Length);
655  if (hmac == NULL) {
656  err_msg = "Unable to calculate HMAC for region.";
657  return false;
658  }
659 
660  hmac = HMAC(EVP_sha256(), messageDigest2, md2Length,
661  reinterpret_cast<unsigned char *>(m_service.data()), m_service.size(), messageDigest.data(),
662  &mdLength);
663  if (hmac == NULL) {
664  err_msg = "Unable to calculate HMAC for service.";
665  return false;
666  }
667 
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);
671  if (hmac == NULL) {
672  err_msg = "Unable to calculate HMAC for request.";
673  return false;
674  }
675 
676  hmac = HMAC(EVP_sha256(), messageDigest2, md2Length,
677  reinterpret_cast<unsigned char *>(stringToSign.data()),
678  stringToSign.size(), messageDigest.data(), &mdLength);
679  if (hmac == NULL) {
680  err_msg = "Unable to calculate HMAC for request string.";
681  return false;
682  }
683 
684  std::string signature;
685  MessageDigestAsHex(messageDigest, signature);
686 
687  auth_token =
688  std::string("AWS4-HMAC-SHA256 Credential=") + keyId + "/" + credentialScope +
689  ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
690  return true;
691 }
692 
693 std::string
694 Factory::GetBucketFromHttpsUrl(const std::string &url) {
695  if (m_url_style == "virtual" || m_url_style.empty()) {
696  // Virtual-hosted-style URLs are of the form https://bucket.region.endpoint/object
697  auto hostname = ExtractHostname(url);
698  if (hostname.empty()) {
699  return {};
700  }
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));
709  }
710  }
711  return {};
712  }
713  return std::string(hostname.substr(0, loc));
714  } else if (m_url_style == "path") {
715  // Path style URLs are of the form https://region.endpoint/bucket/object
716  auto loc = url.find("://");
717  if (loc == std::string::npos) {
718  return {};
719  }
720  loc += 3; // Move past "://"
721  auto slash_loc = url.find('/', loc);
722  if (slash_loc == std::string::npos) {
723  return {};
724  }
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);
729  }
730  return url.substr(bucket_start, bucket_end - bucket_start);
731  } else {
732  // Invalid URL style
733  return {};
734  }
735 }
736 
737 std::tuple<std::string, std::string, bool>
738 Factory::GetCredentialsForBucket(const std::string &bucket, std::string &err_msg)
739 {
740  auto now = std::chrono::steady_clock::now();
741  {
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()) {
745  // If we have credentials for this bucket, check if they are still valid.
746  auto &creds = iter->second.first;
747  auto &expiration = iter->second.second;
748  if (now < expiration) {
749  // Credentials are still valid, return them.
750  return {creds.m_accesskey, creds.m_secretkey, true};
751  }
752  }
753  }
754 
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()) {
759  // If we don't have credentials for this bucket, use the default.
760  if (m_default_creds.m_accesskey.empty() || m_default_creds.m_secretkey.empty()) {
761  // No credentials at all, so we assume public access.
762  m_bucket_auth_map[bucket] = {{"", ""}, now + std::chrono::minutes(1)};
763  return {"", "", true};
764  }
765  access_key_location = m_default_creds.m_accesskey;
766  secret_key_location = m_default_creds.m_secretkey;
767  } else {
768  access_key_location = iter->second.m_accesskey;
769  secret_key_location = iter->second.m_secretkey;
770  }
771  if (access_key_location.empty() && secret_key_location.empty()) {
772  // If both are empty, we assume public access.
773  m_bucket_auth_map[bucket] = {{"", ""}, now + std::chrono::minutes(1)};
774  return {"", "", true};
775  }
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};
780  }
781 
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};
786  }
787  access_key = TrimView(access_key);
788 
789  if (!ReadShortFile(secret_key_location, secret_key, err_msg)) {
790  m_bucket_auth_map[bucket] = {{"", ""}, now + std::chrono::seconds(10)};
791  return {"", "", false};
792  }
793  secret_key = TrimView(secret_key);
794 
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};
799  }
800  m_bucket_auth_map[bucket] = {{access_key, secret_key}, now + std::chrono::minutes(1)};
801  return {access_key, secret_key, true};
802 }
803 
804 std::string
805 Factory::PathEncode(const std::string_view url) {
806  auto loc = url.find("://");
807  if (loc == std::string_view::npos) {
808  return "";
809  }
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)) {
813  // No path, just a query string
814  return "/";
815  }
816  auto path = url.substr(path_loc, query_loc - path_loc);
817  std::string segment;
818  std::string encoded;
819 
820  size_t next = 0;
821  size_t offset = 0;
822  const auto length = path.size();
823  while (offset < length) {
824  next = strcspn(path.data() + offset, "/");
825  if (next == 0) {
826  encoded += "/";
827  offset += 1;
828  continue;
829  }
830  if (offset + next >= length) {
831  next = length - offset;
832  }
833 
834  segment = std::string(path.data() + offset, next);
835  encoded += AmazonURLEncode(segment);
836 
837  offset += next;
838  }
839  return encoded;
840 }
841 
842 // Trim left and right side of a string_view for space characters
843 std::string_view
844 Factory::TrimView(const std::string_view input_view) {
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);
849  }
850  }
851  return "";
852 }
853 
854 extern "C"
855 {
856  XrdEXPORT void *XrdClGetPlugIn(const void*)
857  {
858  return static_cast<void*>(new Factory());
859  }
860 }
XrdVERSIONINFO(XrdClGetPlugIn, XrdClGetPlugIn) using namespace XrdClS3
XrdEXPORT void * XrdClGetPlugIn(const void *)
ssize_t read(int fildes, void *buf, size_t nbyte)
#define close(a)
Definition: XrdPosix.hh:48
#define open
Definition: XrdPosix.hh:78
XrdOucString File
void getline(uchar *buff, int blen)
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)
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::tuple< std::string, std::string, bool > GetCredentialsForBucket(const std::string &bucket, std::string &err_msg)
static std::string_view TrimView(const std::string_view str)
virtual XrdCl::FileSystemPlugIn * CreateFileSystem(const std::string &url) override
Create a file system plug-in for the given URL.
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)
Definition: XrdClEnv.cc:52
bool ImportString(const std::string &key, const std::string &shellKey)
Definition: XrdClEnv.cc:266
bool GetString(const std::string &key, std::string &value)
Definition: XrdClEnv.cc:31
An interface for file plug-ins.
An interface for file plug-ins.
Handle diagnostics.
Definition: XrdClLog.hh:101
void SetTopicName(uint64_t topic, std::string name)
Map a topic number to a string.
Definition: XrdClLog.cc:163
void Warning(uint64_t topic, const char *format,...)
Report a warning.
Definition: XrdClLog.cc:248
void Info(uint64_t topic, const char *format,...)
Print an info.
Definition: XrdClLog.cc:265
std::string_view ltrim_view(const std::string_view &input_view)
const uint64_t kLogXrdClS3