25#include <openssl/evp.h>
26#include <openssl/hmac.h>
36std::shared_mutex Factory::m_bucket_auth_map_mutex;
37bool Factory::m_initialized =
false;
39std::once_flag Factory::m_init_once;
40std::string Factory::m_endpoint =
"";
41std::string Factory::m_service =
"s3";
42std::string Factory::m_region =
"us-east-1";
43std::string Factory::m_url_style =
"virtual";
44std::string Factory::m_mkdir_sentinel;
45Factory::Credentials Factory::m_default_creds;
46std::unordered_map<std::string, Factory::Credentials> Factory::m_bucket_location_map;
47std::unordered_map<std::string, std::pair<Factory::Credentials, std::chrono::steady_clock::time_point>> Factory::m_bucket_auth_map;
53AmazonURLEncode(
const std::string &input) {
60 output.reserve(input.size());
61 for (
const auto & val : input) {
67 if ((
'A' <= val && val <=
'Z') ||
68 (
'a' <= val && val <=
'z') ||
69 (
'0' <= val && val <=
'9') || val ==
'-' ||
70 val ==
'_' || val ==
'.' || val ==
'~') {
71 output.append(1, val);
73 char percentEncode[4];
74 snprintf(percentEncode, 4,
"%%%.2hhX", val);
75 output.append(percentEncode);
84 std::call_once(m_init_once, [&] {
101Factory::CanonicalizeQueryString(
const std::string &url) {
102 auto loc = url.find(
"://");
103 if (loc == std::string::npos) {
107 loc = url.find(
'?', loc);
108 if (loc == std::string::npos) {
111 std::vector<std::pair<std::string, std::string>> query_parameters;
112 auto param_end = url.find(
'&', loc);
113 while (loc != std::string::npos) {
114 auto param_start = loc + 1;
115 loc = url.find(
'=', param_start);
116 if (loc == param_start) {
119 else if (loc >= param_end) {
120 auto param = url.substr(param_start, param_end - param_start);
121 if (!param.empty()) {
123 query_parameters.emplace_back(AmazonURLEncode(param),
"");
126 std::string name = url.substr(param_start, loc - param_start);
128 auto value_start = loc;
130 if (param_end == std::string::npos) {
131 value = url.substr(value_start);
133 value = url.substr(value_start, param_end - value_start);
135 if (!value.empty()) {
136 query_parameters.emplace_back(AmazonURLEncode(name), AmazonURLEncode(value));
140 if (loc != std::string::npos) {
141 param_end = url.find(
'&', loc + 1);
144 std::sort(query_parameters.begin(), query_parameters.end(),
145 [](
const auto &a,
const auto &b) { return a.first < b.first; });
147 size_t string_size = 0;
148 for (
const auto ¶m : query_parameters) {
149 string_size += param.first.size() + param.second.size() + 2;
151 std::string canonicalQueryString;
153 canonicalQueryString.reserve(string_size);
155 for (
const auto ¶m : query_parameters) {
158 canonicalQueryString += param.first +
'=' + param.second;
161 canonicalQueryString +=
'&';
164 if (!canonicalQueryString.empty()) {
165 canonicalQueryString.erase(canonicalQueryString.end() - 1);
167 return canonicalQueryString;
172 if (!m_initialized) {
return nullptr;}
173 return new File(m_log);
178 if (!m_initialized) {
return nullptr;}
184void SetDefault(
XrdCl::Env *env,
const std::string &optName,
const std::string &envName, std::string &location,
const std::string &def) {
186 if (!env->
GetString(optName, val) || val.empty()) {
190 if (env->
GetString(optName, val) && !val.empty()) {
198std::string_view
ltrim_view(
const std::string_view input_view) {
199 for (
size_t idx = 0; idx < input_view.size(); idx++) {
200 if (!isspace(input_view[idx])) {
201 return input_view.substr(idx);
207bool ComputeSHA256(
const std::string_view payload, std::vector<unsigned char> &messageDigest) {
208 EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
213 if (!EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) {
214 EVP_MD_CTX_destroy(mdctx);
218 if (!EVP_DigestUpdate(mdctx, payload.data(), payload.length())) {
219 EVP_MD_CTX_destroy(mdctx);
223 unsigned int mdLength;
224 if (!EVP_DigestFinal_ex(mdctx, messageDigest.data(), &mdLength)) {
225 EVP_MD_CTX_destroy(mdctx);
228 messageDigest.resize(mdLength);
230 EVP_MD_CTX_destroy(mdctx);
234void MessageDigestAsHex(
const std::vector<unsigned char> messageDigest,
235 std::string &hexEncoded) {
236 hexEncoded.resize(messageDigest.size() * 2);
237 char *ptr = hexEncoded.data();
238 for (
unsigned int idx = 0; idx < messageDigest.size(); ++idx, ptr += 2) {
239 snprintf(ptr, 3,
"%02x", messageDigest[idx]);
246ssize_t FullRead(
int fd,
void *ptr,
size_t nbytes) {
247 ssize_t nleft, nread;
252 nread =
read(fd, ptr, nleft);
254 if (errno == EINTR) {
258 }
else if (nread == 0) {
262 ptr =
static_cast<char *
>(ptr) + nread;
264 return (nbytes - nleft);
270ReadShortFile(
const std::string &fileName, std::string &contents, std::string &err_msg) {
271 int fd =
open(fileName.c_str(), O_RDONLY, 0600);
273 err_msg =
"Failed to open file '" + fileName +
"': " + std::string(strerror(errno));
276 contents.resize(32*1024);
278 auto totalRead = FullRead(fd, contents.data(), contents.size());
280 if (totalRead == -1) {
281 err_msg =
"Failed to read file '" + fileName +
"': " + std::string(strerror(errno));
284 contents.resize(totalRead);
292 std::string obj = input_obj;
293 auto loc = input_obj.find(
'?');
294 if (loc != std::string::npos) {
295 auto query = std::string_view(input_obj).substr(loc + 1);
296 obj = obj.substr(0, loc);
297 bool added_query =
false;
298 while (!query.empty()) {
299 auto next_query_loc = query.find(
'&');
300 auto current_query = (next_query_loc == std::string::npos) ? query : query.substr(0, next_query_loc);
301 query = (next_query_loc == std::string::npos) ?
"" : query.substr(next_query_loc + 1);
302 if (current_query.empty()) {
305 auto equal_loc = current_query.find(
'=');
306 if (equal_loc != std::string::npos) {
307 auto key = current_query.substr(0, equal_loc);
308 if (key !=
"authz") {
309 obj += (added_query ?
"&" :
"?") + std::string(current_query);
312 }
else if (current_query !=
"authz") {
313 obj += (added_query ?
"&" :
"?") + std::string(current_query);
325 auto loc = url.find(
"://");
326 if (loc == std::string_view::npos) {
330 auto slash_loc = url.find(
'/', loc);
331 auto query_loc = url.find(
'?', loc);
332 if (query_loc != std::string_view::npos && (slash_loc == std::string_view::npos || query_loc < slash_loc)) {
333 slash_loc = query_loc;
335 auto authority = url.substr(loc, slash_loc - loc);
336 if (authority.empty()) {
339 auto at_loc = authority.find(
'@');
340 if (at_loc != std::string_view::npos) {
342 authority = authority.substr(at_loc + 1);
345 auto colon_loc = authority.find(
':');
346 if (colon_loc != std::string_view::npos) {
347 authority = authority.substr(0, colon_loc);
353Factory::InitS3Config()
356 SetDefault(env,
"XrdClS3MkdirSentinel",
"XRDCLS3_MKDIRSENTINEL", m_mkdir_sentinel,
".xrdcls3.dirsentinel");
357 SetDefault(env,
"XrdClS3Endpoint",
"XRDCLS3_ENDPOINT", m_endpoint,
"");
358 SetDefault(env,
"XrdClS3UrlStyle",
"XRDCLS3_URLSTYLE", m_url_style,
"virtual");
359 SetDefault(env,
"XrdClS3Region",
"XRDCLS3_REGION", m_region,
"us-east-1");
360 std::string access_key;
361 SetDefault(env,
"XrdClS3AccessKeyLocation",
"XRDCLS3_ACCESSKEYLOCATION", access_key,
"");
362 std::string secret_key;
363 SetDefault(env,
"XrdClS3SecretKeyLocation",
"XRDCLS3_SECRETKEYLOCATION", secret_key,
"");
364 if (!access_key.empty() && !secret_key.empty()) {
365 m_default_creds = {access_key, secret_key};
366 }
else if (access_key.empty() && secret_key.empty()) {
367 m_log->Info(
kLogXrdClS3,
"Defaulting to public bucket access");
368 }
else if (access_key.empty() && !secret_key.empty()) {
369 m_log->Warning(
kLogXrdClS3,
"Secret key location set (%s) but access key location is empty; authorization will not work.", secret_key.c_str());
370 }
else if (!access_key.empty() && secret_key.empty()) {
371 m_log->Warning(
kLogXrdClS3,
"Access key location set (%s) but secret key location is empty; authorization will not work.", access_key.c_str());
375 std::string bucket_configs;
376 SetDefault(env,
"XrdClS3BucketConfigs",
"XRDCLS3_BUCKETCONFIGS", bucket_configs,
"");
377 if (!bucket_configs.empty()) {
378 std::stringstream ss(bucket_configs);
379 std::string config_name;
380 while (std::getline(ss, config_name)) {
381 auto name = TrimView(config_name);
382 auto bucket_name_key = std::string(
"XrdClS3") + std::string(name) +
"BucketName";
383 std::string bucket_name_val;
384 if (!env->
GetString(bucket_name_key, bucket_name_val) || bucket_name_val.empty()) {
385 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 auto access_key_location_key = std::string(
"XrdClS3") + std::string(name) +
"AccessKeyLocation";
389 std::string access_key_location_val;
390 auto has_access_key = env->
GetString(access_key_location_key, access_key_location_val) && !access_key_location_val.empty();
392 auto secret_key_location_key = std::string(
"XrdClS3") + std::string(name) +
"SecretKeyLocation";
393 std::string secret_key_location_val;
394 auto has_secret_key = env->
GetString(secret_key_location_key, secret_key_location_val) && !secret_key_location_val.empty();
396 if (has_access_key && has_secret_key) {
397 m_bucket_location_map[bucket_name_val] = {access_key_location_val, secret_key_location_val};
398 }
else if (!has_access_key && !has_secret_key) {
400 m_bucket_location_map[bucket_name_val] = {
"",
""};
401 }
else if (has_access_key && !has_secret_key) {
402 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());
404 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());
412 if (s3_url.substr(0, 5) !=
"s3://") {
413 err_msg =
"Provided URL does not start with s3://";
416 auto loc = s3_url.find(
'/', 5);
417 auto bucket = s3_url.substr(5, loc - 5);
418 auto at_loc = bucket.find(
'@');
419 if (at_loc != std::string::npos) {
420 std::string login =
"";
421 login = bucket.substr(0, at_loc);
422 bucket = bucket.substr(at_loc + 1);
424 std::string endpoint = m_endpoint;
425 std::string region = m_region;
426 if ((bucket == m_endpoint) || m_endpoint.empty()) {
428 auto old_loc = loc + 1;
429 loc = s3_url.find(
'/', loc + 1);
430 if (loc == std::string::npos) {
431 err_msg =
"Provided S3 URL does not contain a bucket in path";
434 bucket = s3_url.substr(old_loc, loc - old_loc);
437 std::string test_endpoint =
"." + endpoint;
438 if (!m_region.empty()) {
439 auto bucket_loc = authority.rfind(
"." + m_region + test_endpoint);
440 if (bucket_loc != std::string::npos) {
441 bucket = authority.substr(0, bucket_loc);
443 auto bucket_loc = authority.rfind(test_endpoint);
444 if (bucket_loc != std::string::npos) {
445 bucket = authority.substr(0, bucket_loc);
449 auto bucket_loc = authority.rfind(test_endpoint);
450 if (bucket_loc != std::string::npos) {
451 bucket = authority.substr(0, bucket_loc);
456 if (loc != std::string::npos) {
457 obj = s3_url.substr(loc + 1);
464 if (m_url_style ==
"virtual" || m_url_style.empty()) {
465 https_url =
"https://" + bucket +
"." + m_region +
"." + endpoint + (obj_result ?
"" : (
"/" + obj));
467 }
else if (m_url_style ==
"path") {
468 if (m_region.empty()) {
469 https_url =
"https://" + m_region +
"." + endpoint +
"/" + bucket + (obj_result ?
"" : (
"/" + obj));
471 https_url =
"https://" + endpoint +
"/" + bucket + (obj_result ?
"" : (
"/" + obj));
475 err_msg =
"Server configuration has invalid setting for URL style";
481Factory::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) {
492 if (secretKey.empty()) {
505 auto canonicalQueryString = CanonicalizeQueryString(url);
509 if (std::find_if(headers.begin(), headers.end(),
510 [](
const auto &pair) { return pair.first ==
"Host"; }) == headers.end()) {
513 err_msg =
"Unable to extract hostname from URL: " + url;
516 headers.emplace_back(
"Host", host);
520 auto iter = std::find_if(headers.begin(), headers.end(),
521 [](
const auto &pair) { return !strcasecmp(pair.first.c_str(),
"X-Amz-Date"); });
522 std::string date_time;
523 char date_char[] =
"YYYYMMDD";
524 if (iter == headers.end()) {
527 struct tm brokenDownTime;
528 gmtime_r(&now, &brokenDownTime);
530 date_time =
"YYYYMMDDThhmmssZ";
531 strftime(date_time.data(), date_time.size(),
"%Y%m%dT%H%M%SZ", &brokenDownTime);
532 headers.emplace_back(
"X-Amz-Date", date_time);
533 strftime(date_char,
sizeof(date_char),
"%Y%m%d", &brokenDownTime);
535 date_time = iter->second;
536 auto loc = date_time.find(
'T', 0);
538 err_msg =
"Invalid value for X-Amz-Date";
541 memcpy(date_char, date_time.c_str(), 8);
547 std::string payload_hash =
"UNSIGNED-PAYLOAD";
548 iter = std::find_if(headers.begin(), headers.end(),
549 [](
const auto &pair) { return !strcasecmp(pair.first.c_str(),
"X-Amz-Content-Sha256"); });
550 if (iter == headers.end()) {
551 headers.emplace_back(
"X-Amz-Content-Sha256", payload_hash);
553 payload_hash = iter->second;
559 std::vector<std::pair<std::string, std::string>> transformed_headers;
560 transformed_headers.reserve(headers.size());
561 for (
const auto &info : headers) {
562 std::string header = info.first;
563 std::transform(header.begin(), header.end(), header.begin(), &tolower);
565 std::string value = info.second;
569 auto value_trimmed = std::string(
TrimView(value));
574 bool inSpaces =
false;
575 while (right < value_trimmed.length()) {
577 if (value_trimmed[right] ==
' ') {
585 if (value_trimmed[right] ==
' ') {
589 value_trimmed.erase(left, right - left - 1);
595 transformed_headers.emplace_back(header, value);
597 std::sort(transformed_headers.begin(), transformed_headers.end(),
598 [](
const auto &a,
const auto &b) { return a.first < b.first; });
602 std::string signedHeaders, canonicalHeaders;
603 for (
const auto &info : transformed_headers) {
604 canonicalHeaders += info.first +
":" + info.second +
"\n";
605 signedHeaders += info.first +
";";
607 signedHeaders.erase(signedHeaders.end() - 1);
610 auto canonicalRequest =
611 verb +
"\n" + canonicalURI +
"\n" + canonicalQueryString +
"\n" +
612 canonicalHeaders +
"\n" + signedHeaders +
"\n" + payload_hash;
619 std::string canonicalRequestHash;
620 std::vector<unsigned char> messageDigest;
621 messageDigest.resize(EVP_MAX_MD_SIZE);
622 if (!ComputeSHA256(canonicalRequest, messageDigest)) {
623 err_msg =
"Unable to hash canonical request.";
626 MessageDigestAsHex(messageDigest, canonicalRequestHash);
629 auto credentialScope = std::string(date_char) +
"/" + m_region +
"/" + m_service +
"/aws4_request";
630 auto stringToSign = std::string(
"AWS4-HMAC-SHA256\n") + date_time +
"\n" + credentialScope +
"\n" + canonicalRequestHash;
638 auto saKey = std::string(
"AWS4") + secretKey;
639 unsigned int mdLength = 0;
640 const unsigned char *hmac =
641 HMAC(EVP_sha256(), saKey.c_str(), saKey.length(), (
unsigned char *)date_char,
642 sizeof(date_char) - 1, messageDigest.data(), &mdLength);
644 err_msg =
"Unable to calculate HMAC for date.";
648 unsigned int md2Length = 0;
649 unsigned char messageDigest2[EVP_MAX_MD_SIZE];
650 hmac = HMAC(EVP_sha256(), messageDigest.data(), mdLength,
651 reinterpret_cast<unsigned char *
>(m_region.data()), m_region.size(), messageDigest2,
654 err_msg =
"Unable to calculate HMAC for region.";
658 hmac = HMAC(EVP_sha256(), messageDigest2, md2Length,
659 reinterpret_cast<unsigned char *
>(m_service.data()), m_service.size(), messageDigest.data(),
662 err_msg =
"Unable to calculate HMAC for service.";
666 const char request_char[] =
"aws4_request";
667 hmac = HMAC(EVP_sha256(), messageDigest.data(), messageDigest.size(),
reinterpret_cast<const unsigned char *
>(request_char),
668 sizeof(request_char) - 1, messageDigest2, &md2Length);
670 err_msg =
"Unable to calculate HMAC for request.";
674 hmac = HMAC(EVP_sha256(), messageDigest2, md2Length,
675 reinterpret_cast<unsigned char *
>(stringToSign.data()),
676 stringToSign.size(), messageDigest.data(), &mdLength);
678 err_msg =
"Unable to calculate HMAC for request string.";
682 std::string signature;
683 MessageDigestAsHex(messageDigest, signature);
686 std::string(
"AWS4-HMAC-SHA256 Credential=") + keyId +
"/" + credentialScope +
687 ",SignedHeaders=" + signedHeaders +
",Signature=" + signature;
693 if (m_url_style ==
"virtual" || m_url_style.empty()) {
696 if (hostname.empty()) {
699 auto test_endpoint =
"." + m_endpoint;
700 if (!m_region.empty()) test_endpoint =
"." + m_region + test_endpoint;
701 auto loc = hostname.rfind(test_endpoint);
702 if (loc == std::string::npos) {
703 if (!m_region.empty()) {
704 loc = hostname.rfind(
"." + m_endpoint);
705 if (loc != std::string::npos) {
706 return std::string(hostname.substr(0, loc));
711 return std::string(hostname.substr(0, loc));
712 }
else if (m_url_style ==
"path") {
714 auto loc = url.find(
"://");
715 if (loc == std::string::npos) {
719 auto slash_loc = url.find(
'/', loc);
720 if (slash_loc == std::string::npos) {
723 auto bucket_start = slash_loc + 1;
724 auto bucket_end = url.find(
'/', bucket_start);
725 if (bucket_end == std::string::npos) {
726 return url.substr(bucket_start);
728 return url.substr(bucket_start, bucket_end - bucket_start);
735std::tuple<std::string, std::string, bool>
738 auto now = std::chrono::steady_clock::now();
740 std::shared_lock lock(m_bucket_auth_map_mutex);
741 auto iter = m_bucket_auth_map.find(bucket);
742 if (iter != m_bucket_auth_map.end()) {
744 auto &creds = iter->second.first;
745 auto &expiration = iter->second.second;
746 if (now < expiration) {
748 return {creds.m_accesskey, creds.m_secretkey,
true};
753 std::unique_lock lock(m_bucket_auth_map_mutex);
754 auto iter = m_bucket_location_map.find(bucket);
755 std::string access_key_location, secret_key_location;
756 if (iter == m_bucket_location_map.end()) {
758 if (m_default_creds.m_accesskey.empty() || m_default_creds.m_secretkey.empty()) {
760 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::minutes(1)};
761 return {
"",
"",
true};
763 access_key_location = m_default_creds.m_accesskey;
764 secret_key_location = m_default_creds.m_secretkey;
766 access_key_location = iter->second.m_accesskey;
767 secret_key_location = iter->second.m_secretkey;
769 if (access_key_location.empty() && secret_key_location.empty()) {
771 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::minutes(1)};
772 return {
"",
"",
true};
774 if (access_key_location.empty() || secret_key_location.empty()) {
775 err_msg =
"No credentials available for bucket: " + bucket;
776 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
777 return {
"",
"",
false};
780 std::string access_key, secret_key;
781 if (!ReadShortFile(access_key_location, access_key, err_msg)) {
782 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
783 return {
"",
"",
false};
787 if (!ReadShortFile(secret_key_location, secret_key, err_msg)) {
788 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
789 return {
"",
"",
false};
793 if (access_key.empty() || secret_key.empty()) {
794 err_msg =
"Credentials for bucket '" + bucket +
"' are empty.";
795 m_bucket_auth_map[bucket] = {{
"",
""}, now + std::chrono::seconds(10)};
796 return {
"",
"",
false};
798 m_bucket_auth_map[bucket] = {{access_key, secret_key}, now + std::chrono::minutes(1)};
799 return {access_key, secret_key,
true};
804 auto loc = url.find(
"://");
805 if (loc == std::string_view::npos) {
808 auto path_loc = url.find(
"/", loc + 3);
809 auto query_loc = url.find(
"?", loc + 3);
810 if (query_loc != std::string_view::npos && (path_loc == std::string_view::npos || query_loc < path_loc)) {
814 auto path = url.substr(path_loc, query_loc - path_loc);
820 const auto length = path.size();
821 while (offset < length) {
822 next = strcspn(path.data() + offset,
"/");
828 if (offset + next >= length) {
829 next = length - offset;
832 segment = std::string(path.data() + offset, next);
833 encoded += AmazonURLEncode(segment);
843 auto view = ltrim_view(input_view);
844 for (
size_t idx = 0; idx < input_view.size(); idx++) {
845 if (!isspace(view[view.size() - 1 - idx])) {
846 return view.substr(0, view.size() - idx);
856 return static_cast<void*
>(
new Factory());
void * XrdClGetPlugIn(const void *)
void * XrdClGetPlugIn(const void *)
XrdVERSIONINFO(XrdClGetPlugIn, XrdClGetPlugIn) using namespace XrdClS3
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