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