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