XRootD
XrdMacaroonsHandler.cc
Go to the documentation of this file.
1 
2 #include <cstring>
3 #include <string>
4 #include <iostream>
5 #include <sstream>
6 
7 #include <uuid/uuid.h>
8 #include "json.h"
9 #include "macaroons.h"
10 
11 #include "XrdAcc/XrdAccPrivs.hh"
13 #include "XrdSys/XrdSysError.hh"
14 #include "XrdSec/XrdSecEntity.hh"
15 
16 #include "XrdMacaroonsHandler.hh"
17 
18 #include "XrdOuc/XrdOucTUtils.hh"
19 
20 using namespace Macaroons;
21 
22 
23 char *unquote(const char *str) {
24  int l = strlen(str);
25  char *r = (char *) malloc(l + 1);
26  r[0] = '\0';
27  int i, j = 0;
28 
29  for (i = 0; i < l; i++) {
30 
31  if (str[i] == '%') {
32  char savec[3];
33  if (l <= i + 3) {
34  free(r);
35  return NULL;
36  }
37  savec[0] = str[i + 1];
38  savec[1] = str[i + 2];
39  savec[2] = '\0';
40 
41  r[j] = strtol(savec, 0, 16);
42 
43  i += 2;
44  } else if (str[i] == '+') r[j] = ' ';
45  else r[j] = str[i];
46 
47  j++;
48  }
49 
50  r[j] = '\0';
51 
52  return r;
53 
54 }
55 
56 
57 std::string Macaroons::NormalizeSlashes(const std::string &input)
58 {
59  std::string output;
60  // In most cases, the output should be "about as large"
61  // as the input
62  output.reserve(input.size());
63  char prior_chr = '\0';
64  size_t output_idx = 0;
65  for (size_t idx = 0; idx < input.size(); idx++) {
66  char chr = input[idx];
67  if (prior_chr == '/' && chr == '/') {
68  output_idx++;
69  continue;
70  }
71  output += input[output_idx];
72  prior_chr = chr;
73  output_idx++;
74  }
75  return output;
76 }
77 
78 static
79 ssize_t determine_validity(const std::string& input)
80 {
81  ssize_t duration = 0;
82  if (input.find("PT") != 0)
83  {
84  return -1;
85  }
86  size_t pos = 2;
87  std::string remaining = input;
88  do
89  {
90  remaining = remaining.substr(pos);
91  if (remaining.size() == 0) break;
92  long cur_duration;
93  try
94  {
95  cur_duration = stol(remaining, &pos);
96  } catch (...)
97  {
98  return -1;
99  }
100  if (pos >= remaining.size())
101  {
102  return -1;
103  }
104  char unit = remaining[pos];
105  switch (unit) {
106  case 'S':
107  break;
108  case 'M':
109  cur_duration *= 60;
110  break;
111  case 'H':
112  cur_duration *= 3600;
113  break;
114  default:
115  return -1;
116  };
117  pos ++;
118  duration += cur_duration;
119  } while (1);
120  return duration;
121 }
122 
123 
125 {
126  delete m_chain;
127 }
128 
129 
130 std::string
131 Handler::GenerateID(const std::string &resource,
132  const XrdSecEntity &entity,
133  const std::string &activities,
134  const std::vector<std::string> &other_caveats,
135  const std::string &before)
136 {
137  uuid_t uu;
138  uuid_generate_random(uu);
139  char uuid_buf[37];
140  uuid_unparse(uu, uuid_buf);
141  std::string result(uuid_buf);
142 
143 // The following code shoul have been strictly for debugging purposes. This
144 // added code skips it unless debug logging has been enabled. Due to the code
145 // structure, indentation is a bit of a struggle as this is a minimal fix.
146 //
147 if (m_log->getMsgMask() & LogMask::Debug)
148  {
149  std::stringstream ss;
150  ss << "ID=" << result << ", ";
151  ss << "resource=" << NormalizeSlashes(resource) << ", ";
152  if (entity.prot[0] != '\0') {ss << "protocol=" << entity.prot << ", ";}
153  if (entity.name) {ss << "name=" << entity.name << ", ";}
154  if (entity.host) {ss << "host=" << entity.host << ", ";}
155  if (entity.vorg) {ss << "vorg=" << entity.vorg << ", ";}
156  if (entity.role) {ss << "role=" << entity.role << ", ";}
157  if (entity.grps) {ss << "groups=" << entity.grps << ", ";}
158  if (entity.endorsements) {ss << "endorsements=" << entity.endorsements << ", ";}
159  if (activities.size()) {ss << "base_activities=" << activities << ", ";}
160 
161  for (std::vector<std::string>::const_iterator iter = other_caveats.begin();
162  iter != other_caveats.end();
163  iter++)
164  {
165  ss << "user_caveat=" << *iter << ", ";
166  }
167 
168  ss << "expires=" << before;
169 
170  m_log->Emsg("MacaroonGen", ss.str().c_str()); // Mask::Debug
171  }
172  return result;
173 }
174 
175 
176 std::string
177 Handler::GenerateActivities(const XrdHttpExtReq & req, const std::string &resource) const
178 {
179  std::string result = "activity:READ_METADATA";
180  // TODO - generate environment object that includes the Authorization header.
181  XrdAccPrivs privs = m_chain ? m_chain->Access(&req.GetSecEntity(), resource.c_str(), AOP_Any, NULL) : XrdAccPriv_None;
182  if ((privs & XrdAccPriv_Create) == XrdAccPriv_Create) {result += ",UPLOAD";}
183  if (privs & XrdAccPriv_Read) {result += ",DOWNLOAD";}
184  if (privs & XrdAccPriv_Delete) {result += ",DELETE";}
185  if ((privs & XrdAccPriv_Chown) == XrdAccPriv_Chown) {result += ",MANAGE,UPDATE_METADATA";}
186  if (privs & XrdAccPriv_Readdir) {result += ",LIST";}
187  return result;
188 }
189 
190 
191 // See if the macaroon handler is interested in this request.
192 // We intercept all POST requests as we will be looking for a particular
193 // header.
194 bool
195 Handler::MatchesPath(const char *verb, const char *path)
196 {
197  return !strcmp(verb, "POST") || !strncmp(path, "/.well-known/", 13) ||
198  !strncmp(path, "/.oauth2/", 9);
199 }
200 
201 
202 int Handler::ProcessOAuthConfig(XrdHttpExtReq &req) {
203  if (req.verb != "GET")
204  {
205  return req.SendSimpleResp(405, NULL, NULL, "Only GET is valid for oauth config.", 0);
206  }
207  auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"host");
208  if (header == req.headers.end())
209  {
210  return req.SendSimpleResp(400, NULL, NULL, "Host header is required.", 0);
211  }
212 
213  json_object *response_obj = json_object_new_object();
214  if (!response_obj)
215  {
216  return req.SendSimpleResp(500, NULL, NULL, "Unable to create new JSON response object.", 0);
217  }
218  std::string token_endpoint = "https://" + header->second + "/.oauth2/token";
219  json_object *endpoint_obj =
220  json_object_new_string_len(token_endpoint.c_str(), token_endpoint.size());
221  if (!endpoint_obj)
222  {
223  return req.SendSimpleResp(500, NULL, NULL, "Unable to create a new JSON macaroon string.", 0);
224  }
225  json_object_object_add(response_obj, "token_endpoint", endpoint_obj);
226 
227  const char *response_result = json_object_to_json_string_ext(response_obj, JSON_C_TO_STRING_PRETTY);
228  int retval = req.SendSimpleResp(200, NULL, NULL, response_result, 0);
229  json_object_put(response_obj);
230  return retval;
231 }
232 
233 
234 int Handler::ProcessTokenRequest(XrdHttpExtReq &req)
235 {
236  if (req.verb != "POST")
237  {
238  return req.SendSimpleResp(405, NULL, NULL, "Only POST is valid for token request.", 0);
239  }
240  auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-type");
241  if (header == req.headers.end())
242  {
243  return req.SendSimpleResp(400, NULL, NULL, "Content-Type missing; not a valid macaroon request?", 0);
244  }
245  if (header->second != "application/x-www-form-urlencoded")
246  {
247  return req.SendSimpleResp(400, NULL, NULL, "Content-Type must be set to `application/macaroon-request' to request a macaroon", 0);
248  }
249  char *request_data_raw;
250  // Note: this does not null-terminate the buffer contents.
251  if (req.BuffgetData(req.length, &request_data_raw, true) != req.length)
252  {
253  return req.SendSimpleResp(400, NULL, NULL, "Missing or invalid body of request.", 0);
254  }
255  std::string request_data(request_data_raw, req.length);
256  bool found_grant_type = false;
257  ssize_t validity = -1;
258  std::string scope;
259  std::string token;
260  std::istringstream token_stream(request_data);
261  while (std::getline(token_stream, token, '&'))
262  {
263  std::string::size_type eq = token.find("=");
264  if (eq == std::string::npos)
265  {
266  return req.SendSimpleResp(400, NULL, NULL, "Invalid format for form-encoding", 0);
267  }
268  std::string key = token.substr(0, eq);
269  std::string value = token.substr(eq + 1);
270  //std::cout << "Found key " << key << ", value " << value << std::endl;
271  if (key == "grant_type")
272  {
273  found_grant_type = true;
274  if (value != "client_credentials")
275  {
276  return req.SendSimpleResp(400, NULL, NULL, "Invalid grant type specified.", 0);
277  }
278  }
279  else if (key == "expire_in")
280  {
281  try
282  {
283  validity = std::stoll(value);
284  }
285  catch (...)
286  {
287  return req.SendSimpleResp(400, NULL, NULL, "Expiration request not parseable.", 0);
288  }
289  if (validity <= 0)
290  {
291  return req.SendSimpleResp(400, NULL, NULL, "Expiration request has invalid value.", 0);
292  }
293  }
294  else if (key == "scope")
295  {
296  char *value_raw = unquote(value.c_str());
297  if (value_raw == NULL)
298  {
299  return req.SendSimpleResp(400, NULL, NULL, "Unable to unquote scope.", 0);
300  }
301  scope = value_raw;
302  free(value_raw);
303  }
304  }
305  if (!found_grant_type)
306  {
307  return req.SendSimpleResp(400, NULL, NULL, "Grant type not specified.", 0);
308  }
309  if (scope.empty())
310  {
311  return req.SendSimpleResp(400, NULL, NULL, "Scope was not specified.", 0);
312  }
313  std::istringstream token_stream_scope(scope);
314  std::string path;
315  std::vector<std::string> other_caveats;
316  while (std::getline(token_stream_scope, token, ' '))
317  {
318  std::string::size_type col = token.find(":");
319  if (col == std::string::npos)
320  {
321  return req.SendSimpleResp(400, NULL, NULL, "Invalid format for requested scope", 0);
322  }
323  std::string key = token.substr(0, col);
324  std::string value = token.substr(col + 1);
325  //std::cout << "Found activity " << key << ", path " << value << std::endl;
326  if (path.empty())
327  {
328  path = value;
329  }
330  else if (value != path)
331  {
332  if (m_log->getMsgMask() & LogMask::Error) {
333  std::stringstream ss;
334  ss << "Encountered requested scope request for authorization " << key
335  << " with resource path " << value << "; however, prior request had path "
336  << path;
337  m_log->Emsg("MacaroonRequest", ss.str().c_str()); // Mask::Error
338  }
339  return req.SendSimpleResp(500, NULL, NULL, "Server only supports all scopes having the same path", 0);
340  }
341  other_caveats.push_back(key);
342  }
343  if (path.empty())
344  {
345  path = "/";
346  }
347  std::vector<std::string> other_caveats_final;
348  if (!other_caveats.empty()) {
349  std::stringstream ss;
350  ss << "activity:";
351  for (std::vector<std::string>::const_iterator iter = other_caveats.begin();
352  iter != other_caveats.end();
353  iter++)
354  {
355  ss << *iter << ",";
356  }
357  const std::string &final_str = ss.str();
358  other_caveats_final.push_back(final_str.substr(0, final_str.size() - 1));
359  }
360  return GenerateMacaroonResponse(req, path, other_caveats_final, validity, true);
361 }
362 
363 
364 // Process a macaroon request.
366 {
367  if (req.resource == "/.well-known/oauth-authorization-server") {
368  return ProcessOAuthConfig(req);
369  } else if (req.resource == "/.oauth2/token") {
370  return ProcessTokenRequest(req);
371  }
372 
373  auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-type");
374  if (header == req.headers.end())
375  {
376  return req.SendSimpleResp(400, NULL, NULL, "Content-Type missing; not a valid macaroon request?", 0);
377  }
378  if (header->second != "application/macaroon-request")
379  {
380  return req.SendSimpleResp(400, NULL, NULL, "Content-Type must be set to `application/macaroon-request' to request a macaroon", 0);
381  }
382  header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-length");
383  if (header == req.headers.end())
384  {
385  return req.SendSimpleResp(400, NULL, NULL, "Content-Length missing; not a valid POST", 0);
386  }
387  ssize_t blen;
388  try
389  {
390  blen = std::stoll(header->second);
391  }
392  catch (...)
393  {
394  return req.SendSimpleResp(400, NULL, NULL, "Content-Length not parseable.", 0);
395  }
396  if (blen <= 0)
397  {
398  return req.SendSimpleResp(400, NULL, NULL, "Content-Length has invalid value.", 0);
399  }
400  //for (const auto &header : req.headers) { printf("** Request header: %s=%s\n", header.first.c_str(), header.second.c_str()); }
401 
402  // request_data is not necessarily null-terminated; hence, we use the more advanced _ex variant
403  // of the tokener to avoid making a copy of the character buffer.
404  char *request_data;
405  if (req.BuffgetData(blen, &request_data, true) != blen)
406  {
407  return req.SendSimpleResp(400, NULL, NULL, "Missing or invalid body of request.", 0);
408  }
409  json_tokener *tokener = json_tokener_new();
410  if (!tokener)
411  {
412  return req.SendSimpleResp(500, NULL, NULL, "Internal error when allocating token parser.", 0);
413  }
414  json_object *macaroon_req = json_tokener_parse_ex(tokener, request_data, blen);
415  enum json_tokener_error err = json_tokener_get_error(tokener);
416  json_tokener_free(tokener);
417  if (err != json_tokener_success)
418  {
419  if (macaroon_req) json_object_put(macaroon_req);
420  return req.SendSimpleResp(400, NULL, NULL, "Invalid JSON serialization of macaroon request.", 0);
421  }
422  json_object *validity_obj;
423  if (!json_object_object_get_ex(macaroon_req, "validity", &validity_obj))
424  {
425  json_object_put(macaroon_req);
426  return req.SendSimpleResp(400, NULL, NULL, "JSON request does not include a `validity`", 0);
427  }
428  const char *validity_cstr = json_object_get_string(validity_obj);
429  if (!validity_cstr)
430  {
431  json_object_put(macaroon_req);
432  return req.SendSimpleResp(400, NULL, NULL, "validity key cannot be cast to a string", 0);
433  }
434  std::string validity_str(validity_cstr);
435  ssize_t validity = determine_validity(validity_str);
436  if (validity <= 0)
437  {
438  json_object_put(macaroon_req);
439  return req.SendSimpleResp(400, NULL, NULL, "Invalid ISO 8601 duration for validity key", 0);
440  }
441  json_object *caveats_obj;
442  std::vector<std::string> other_caveats;
443  if (json_object_object_get_ex(macaroon_req, "caveats", &caveats_obj))
444  {
445  if (json_object_is_type(caveats_obj, json_type_array))
446  { // Caveats were provided. Let's record them.
447  // TODO - could just add these in-situ. No need for the other_caveats vector.
448  int array_length = json_object_array_length(caveats_obj);
449  other_caveats.reserve(array_length);
450  for (int idx=0; idx<array_length; idx++)
451  {
452  json_object *caveat_item = json_object_array_get_idx(caveats_obj, idx);
453  if (caveat_item)
454  {
455  const char *caveat_item_str = json_object_get_string(caveat_item);
456  other_caveats.emplace_back(caveat_item_str);
457  }
458  }
459  }
460  }
461  json_object_put(macaroon_req);
462 
463  return GenerateMacaroonResponse(req, req.resource, other_caveats, validity, false);
464 }
465 
466 
467 int
468 Handler::GenerateMacaroonResponse(XrdHttpExtReq &req, const std::string &resource,
469  const std::vector<std::string> &other_caveats, ssize_t validity, bool oauth_response)
470 {
471  time_t now;
472  time(&now);
473  if (m_max_duration > 0)
474  {
475  validity = (validity > m_max_duration) ? m_max_duration : validity;
476  }
477  now += validity;
478 
479  char utc_time_buf[21];
480  if (!strftime(utc_time_buf, 21, "%FT%TZ", gmtime(&now)))
481  {
482  return req.SendSimpleResp(500, NULL, NULL, "Internal error constructing UTC time", 0);
483  }
484  std::string utc_time_str(utc_time_buf);
485  std::stringstream ss;
486  ss << "before:" << utc_time_str;
487  std::string utc_time_caveat = ss.str();
488 
489  std::string activities = GenerateActivities(req, resource);
490  std::string macaroon_id = GenerateID(resource, req.GetSecEntity(), activities, other_caveats, utc_time_str);
491  enum macaroon_returncode mac_err;
492 
493  struct macaroon *mac = macaroon_create(reinterpret_cast<const unsigned char*>(m_location.c_str()),
494  m_location.size(),
495  reinterpret_cast<const unsigned char*>(m_secret.c_str()),
496  m_secret.size(),
497  reinterpret_cast<const unsigned char*>(macaroon_id.c_str()),
498  macaroon_id.size(), &mac_err);
499  if (!mac) {
500  return req.SendSimpleResp(500, NULL, NULL, "Internal error constructing the macaroon", 0);
501  }
502 
503  // Embed the SecEntity name, if present.
504  struct macaroon *mac_with_name;
505  const char * sec_name = req.GetSecEntity().name;
506  if (sec_name) {
507  std::stringstream name_caveat_ss;
508  name_caveat_ss << "name:" << sec_name;
509  std::string name_caveat = name_caveat_ss.str();
510  mac_with_name = macaroon_add_first_party_caveat(mac,
511  reinterpret_cast<const unsigned char*>(name_caveat.c_str()),
512  name_caveat.size(),
513  &mac_err);
514  macaroon_destroy(mac);
515  } else {
516  mac_with_name = mac;
517  }
518  if (!mac_with_name)
519  {
520  return req.SendSimpleResp(500, NULL, NULL, "Internal error adding default activities to macaroon", 0);
521  }
522 
523  struct macaroon *mac_with_activities = macaroon_add_first_party_caveat(mac_with_name,
524  reinterpret_cast<const unsigned char*>(activities.c_str()),
525  activities.size(),
526  &mac_err);
527  macaroon_destroy(mac_with_name);
528  if (!mac_with_activities)
529  {
530  return req.SendSimpleResp(500, NULL, NULL, "Internal error adding default activities to macaroon", 0);
531  }
532 
533 
534  for (const auto &caveat : other_caveats)
535  {
536  struct macaroon *mac_tmp = mac_with_activities;
537  mac_with_activities = macaroon_add_first_party_caveat(mac_tmp,
538  reinterpret_cast<const unsigned char*>(caveat.c_str()),
539  caveat.size(),
540  &mac_err);
541  macaroon_destroy(mac_tmp);
542  if (!mac_with_activities)
543  {
544  return req.SendSimpleResp(500, NULL, NULL, "Internal error adding user caveat to macaroon", 0);
545  }
546  }
547 
548  // Note we don't call `NormalizeSlashes` here; for backward compatibility reasons, we ensure the
549  // token issued is identical to what was working with prior versions of XRootD. This allows for a
550  // mix of old/new versions in a single cluster to interoperate. In a few years, it might be reasonable
551  // to invoke it here as well.
552  std::string path_caveat = "path:" + resource;
553  struct macaroon *mac_with_path = macaroon_add_first_party_caveat(mac_with_activities,
554  reinterpret_cast<const unsigned char*>(path_caveat.c_str()),
555  path_caveat.size(),
556  &mac_err);
557  macaroon_destroy(mac_with_activities);
558  if (!mac_with_path) {
559  return req.SendSimpleResp(500, NULL, NULL, "Internal error adding path to macaroon", 0);
560  }
561 
562  struct macaroon *mac_with_date = macaroon_add_first_party_caveat(mac_with_path,
563  reinterpret_cast<const unsigned char*>(utc_time_caveat.c_str()),
564  utc_time_caveat.size(),
565  &mac_err);
566  macaroon_destroy(mac_with_path);
567  if (!mac_with_date) {
568  return req.SendSimpleResp(500, NULL, NULL, "Internal error adding date to macaroon", 0);
569  }
570 
571  size_t size_hint = macaroon_serialize_size_hint(mac_with_date);
572 
573  std::vector<char> macaroon_resp; macaroon_resp.resize(size_hint);
574  if (macaroon_serialize(mac_with_date, &macaroon_resp[0], size_hint, &mac_err))
575  {
576  printf("Returned macaroon_serialize code: %zu\n", size_hint);
577  return req.SendSimpleResp(500, NULL, NULL, "Internal error serializing macaroon", 0);
578  }
579  macaroon_destroy(mac_with_date);
580 
581  json_object *response_obj = json_object_new_object();
582  if (!response_obj)
583  {
584  return req.SendSimpleResp(500, NULL, NULL, "Unable to create new JSON response object.", 0);
585  }
586  json_object *macaroon_obj = json_object_new_string_len(&macaroon_resp[0], strlen(&macaroon_resp[0]));
587  if (!macaroon_obj)
588  {
589  return req.SendSimpleResp(500, NULL, NULL, "Unable to create a new JSON macaroon string.", 0);
590  }
591  json_object_object_add(response_obj, oauth_response ? "access_token" : "macaroon", macaroon_obj);
592 
593  json_object *expire_in_obj = json_object_new_int64(validity);
594  if (!expire_in_obj)
595  {
596  return req.SendSimpleResp(500, NULL, NULL, "Unable to create a new JSON validity object.", 0);
597  }
598  json_object_object_add(response_obj, "expires_in", expire_in_obj);
599 
600  const char *macaroon_result = json_object_to_json_string_ext(response_obj, JSON_C_TO_STRING_PRETTY);
601  int retval = req.SendSimpleResp(200, NULL, NULL, macaroon_result, 0);
602  json_object_put(response_obj);
603  return retval;
604 }
@ AOP_Any
Special for getting privs.
XrdAccPrivs
Definition: XrdAccPrivs.hh:39
@ XrdAccPriv_Chown
Definition: XrdAccPrivs.hh:41
@ XrdAccPriv_Read
Definition: XrdAccPrivs.hh:49
@ XrdAccPriv_None
Definition: XrdAccPrivs.hh:53
@ XrdAccPriv_Delete
Definition: XrdAccPrivs.hh:43
@ XrdAccPriv_Create
Definition: XrdAccPrivs.hh:42
@ XrdAccPriv_Readdir
Definition: XrdAccPrivs.hh:50
char * unquote(const char *str)
static ssize_t determine_validity(const std::string &input)
void getline(uchar *buff, int blen)
virtual bool MatchesPath(const char *verb, const char *path) override
Tells if the incoming path is recognized as one of the paths that have to be processed.
virtual int ProcessReq(XrdHttpExtReq &req) override
virtual XrdAccPrivs Access(const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *Env=0)=0
std::map< std::string, std::string > & headers
std::string resource
std::string verb
int BuffgetData(int blen, char **data, bool wait)
Get a pointer to data read from the client, valid for up to blen bytes from the buffer....
const XrdSecEntity & GetSecEntity() const
int SendSimpleResp(int code, const char *desc, const char *header_to_add, const char *body, long long bodylen)
Sends a basic response. If the length is < 0 then it is calculated internally.
static std::map< std::string, T >::const_iterator caseInsensitiveFind(const std::map< std::string, T > &m, const std::string &lowerCaseSearchKey)
Definition: XrdOucTUtils.hh:79
char * vorg
Entity's virtual organization(s)
Definition: XrdSecEntity.hh:71
char prot[XrdSecPROTOIDSIZE]
Auth protocol used (e.g. krb5)
Definition: XrdSecEntity.hh:67
char * grps
Entity's group name(s)
Definition: XrdSecEntity.hh:73
char * name
Entity's name.
Definition: XrdSecEntity.hh:69
char * role
Entity's role(s)
Definition: XrdSecEntity.hh:72
char * endorsements
Protocol specific endorsements.
Definition: XrdSecEntity.hh:75
char * host
Entity's host name dnr dependent.
Definition: XrdSecEntity.hh:70
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
Definition: XrdSysError.cc:95
int getMsgMask()
Definition: XrdSysError.hh:156
std::string NormalizeSlashes(const std::string &)