XRootD
XrdHttpTapeApi.cc
Go to the documentation of this file.
1 /******************************************************************************/
2 /* */
3 /* X r d H t t p T a p e A p i . c c */
4 /* */
5 /* (c) 2026 by the XRootD Collaboration */
6 /* */
7 /* This file is part of the XRootD software suite. */
8 /* */
9 /* XRootD is free software: you can redistribute it and/or modify it under */
10 /* the terms of the GNU Lesser General Public License as published by the */
11 /* Free Software Foundation, either version 3 of the License, or (at your */
12 /* option) any later version. */
13 /* */
14 /* XRootD is distributed in the hope that it will be useful, but WITHOUT */
15 /* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
16 /* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
17 /* License for more details. */
18 /* */
19 /* You should have received a copy of the GNU Lesser General Public License */
20 /* along with XRootD in a file called COPYING.LESSER (LGPL license) and file */
21 /* COPYING (GPL license). If not, see <http://www.gnu.org/licenses/>. */
22 /* */
23 /******************************************************************************/
24 
26 #include "XrdHttpTapeApiStore.hh"
27 #include "XrdOuc/XrdOuca2x.hh"
28 #include "XrdOuc/XrdOucJson.hh"
29 #include "XrdOuc/XrdOucTUtils.hh"
31 #include "XrdSys/XrdSysError.hh"
32 #include "XrdVersion.hh"
33 
34 #include <climits>
35 #include <cstdlib>
36 #include <exception>
37 #include <string>
38 
39 namespace
40 {
41 using Json = nlohmann::json;
42 
43 constexpr char kDiscoveryPath[] = "/.well-known/wlcg-tape-rest-api";
44 constexpr char kStagePath[] = "/api/v1/stage";
45 constexpr char kStagePrefix[] = "/api/v1/stage/";
46 constexpr char kStageCancelSuffix[] = "/cancel";
47 constexpr char kReleasePrefix[] = "/api/v1/release/";
48 constexpr char kArchiveInfoPath[] = "/api/v1/archiveinfo";
49 constexpr long long kDefaultMaxRequestSize = 4 * 1024 * 1024;
50 
51 class TapeApiHandler final : public XrdHttpExtHandler
52 {
53  public:
54  TapeApiHandler(const std::string &root, long long maxRequestSize,
55  const std::string &siteName)
56  : m_store(root), m_maxRequestSize(maxRequestSize),
57  m_siteName(siteName) {}
58 
59  bool MatchesPath(const char *verb, const char *path) override;
60  int ProcessReq(XrdHttpExtReq &req) override;
61  int Init(const char *cfgfile) override;
62  const std::string &InitializationError() const { return m_initError; }
63 
64  private:
65  static int SendJson(XrdHttpExtReq &req, int code,
66  const std::string &body,
67  const std::string &additionalHeaders = {});
68  static int SendError(XrdHttpExtReq &req, int code,
69  const std::string &message);
70  static int SendStatus(XrdHttpExtReq &req,
71  const XrdHttpTapeApiStore::Status &status,
72  const std::string &body = {});
73  bool ReadBody(XrdHttpExtReq &req, std::string &body,
74  int &errorCode, std::string &error);
75  static bool ParseJsonBody(const std::string &body, Json &json,
76  std::string &error);
77  static bool ParsePaths(const std::string &body, Json &paths,
78  std::string &error);
79  static bool ExtractRequestId(const std::string &resource,
80  const std::string &prefix,
81  std::string &requestId);
82  int Discovery(XrdHttpExtReq &req);
83  int Stage(XrdHttpExtReq &req, const std::string &body);
84  int StageStatus(XrdHttpExtReq &req, const std::string &requestId);
85  int StageCancel(XrdHttpExtReq &req, const std::string &requestId,
86  const std::string &body);
87  int StageDelete(XrdHttpExtReq &req, const std::string &requestId);
88  int Release(XrdHttpExtReq &req, const std::string &requestId,
89  const std::string &body);
90  int ArchiveInfo(XrdHttpExtReq &req, const std::string &body);
91 
92  XrdHttpTapeApiStore m_store;
93  long long m_maxRequestSize;
94  std::string m_siteName;
95  std::string m_initError;
96 };
97 
98 int TapeApiHandler::SendJson(XrdHttpExtReq &req, int code,
99  const std::string &body,
100  const std::string &additionalHeaders)
101 {
102  std::string headers = "Content-Type: application/json";
103  if(!additionalHeaders.empty())
104  {
105  headers += "\r\n";
106  headers += additionalHeaders;
107  }
108  // A null description makes XrdHttp fill in the standard reason phrase.
109  return req.SendSimpleResp(code, nullptr, headers.c_str(),
110  body.c_str(), body.size());
111 }
112 
113 int TapeApiHandler::SendError(XrdHttpExtReq &req, int code,
114  const std::string &message)
115 {
116  const std::string body = Json({{"status", code}, {"title", message}}).dump();
117  return req.SendSimpleResp(code, nullptr,
118  "Content-Type: application/problem+json", body.c_str(), body.size());
119 }
120 
121 int TapeApiHandler::SendStatus(
122  XrdHttpExtReq &req, const XrdHttpTapeApiStore::Status &status,
123  const std::string &body)
124 {
125  return status ? SendJson(req, status.code, body)
126  : SendError(req, status.code, status.message);
127 }
128 
129 bool TapeApiHandler::ReadBody(XrdHttpExtReq &req, std::string &body,
130  int &errorCode, std::string &error)
131 {
132  body.clear();
133  if(req.length == 0) return true;
134  if(req.length < 0)
135  {
136  errorCode = 400;
137  error = "invalid negative request length";
138  return false;
139  }
140  if(req.length > m_maxRequestSize)
141  {
142  errorCode = 413;
143  error = "request too large";
144  return false;
145  }
146 
147  // BuffgetData returns a borrowed view into the protocol buffer; it does not
148  // transfer ownership to the handler.
149  char *buffer = nullptr;
150  const int bytesRead = req.BuffgetData(
151  static_cast<int>(req.length), &buffer, true);
152  if(bytesRead != req.length || buffer == nullptr)
153  {
154  errorCode = 400;
155  error = "missing or invalid request body";
156  return false;
157  }
158  body.assign(buffer, buffer + bytesRead);
159  return true;
160 }
161 
162 bool TapeApiHandler::ParseJsonBody(const std::string &body, Json &json,
163  std::string &error)
164 {
165  try
166  {
167  json = Json::parse(body);
168  return true;
169  }
170  catch(const std::exception &ex)
171  {
172  error = "malformed JSON request: " + std::string(ex.what());
173  return false;
174  }
175 }
176 
177 bool TapeApiHandler::ParsePaths(const std::string &body, Json &paths,
178  std::string &error)
179 {
180  Json json;
181  if(!ParseJsonBody(body, json, error)) return false;
182  if(!json.is_object() || !json.contains("paths")
183  || !json["paths"].is_array() || json["paths"].empty())
184  {
185  error = "request must contain a non-empty paths array";
186  return false;
187  }
188  for(const auto &item : json["paths"])
189  {
190  if(!item.is_string() || item.get<std::string>().empty())
191  {
192  error = "paths entries must be non-empty strings";
193  return false;
194  }
195  }
196  paths = std::move(json["paths"]);
197  return true;
198 }
199 
200 bool TapeApiHandler::ExtractRequestId(const std::string &resource,
201  const std::string &prefix,
202  std::string &requestId)
203 {
204  if(resource.compare(0, prefix.size(), prefix) != 0) return false;
205  requestId = resource.substr(prefix.size());
206  return !requestId.empty() && requestId.find('/') == std::string::npos;
207 }
208 
209 bool TapeApiHandler::MatchesPath(const char * /*verb*/, const char *path)
210 {
211  if(!path) return false;
212 
213  const std::string resource(path);
214  return resource == kDiscoveryPath
215  || resource == kStagePath
216  || resource.compare(0, sizeof(kStagePrefix) - 1, kStagePrefix) == 0
217  || resource.compare(0, sizeof(kReleasePrefix) - 1,
218  kReleasePrefix) == 0
219  || resource == kArchiveInfoPath;
220 }
221 
222 int TapeApiHandler::ProcessReq(XrdHttpExtReq &req)
223 {
224  const std::string resource = req.resource;
225 
226  std::string body;
227  if(req.verb == "POST")
228  {
229  int errorCode = 400;
230  std::string error;
231  if(!ReadBody(req, body, errorCode, error))
232  {
233  return SendError(req, errorCode, error);
234  }
235  }
236 
237  if(resource == kDiscoveryPath) return Discovery(req);
238  if(resource == kStagePath) return Stage(req, body);
239  if(resource == kArchiveInfoPath) return ArchiveInfo(req, body);
240 
241  std::string requestId;
242  if(resource.compare(0, sizeof(kStagePrefix) - 1, kStagePrefix) == 0)
243  {
244  if(resource.size() > sizeof(kStageCancelSuffix) - 1
245  && resource.compare(resource.size() - (sizeof(kStageCancelSuffix) - 1),
246  sizeof(kStageCancelSuffix) - 1,
247  kStageCancelSuffix) == 0)
248  {
249  const std::string stageResource = resource.substr(
250  0, resource.size() - (sizeof(kStageCancelSuffix) - 1));
251  if(ExtractRequestId(stageResource, kStagePrefix, requestId))
252  {
253  return StageCancel(req, requestId, body);
254  }
255  }
256  else if(ExtractRequestId(resource, kStagePrefix, requestId))
257  {
258  return req.verb == "DELETE" ? StageDelete(req, requestId)
259  : StageStatus(req, requestId);
260  }
261  return SendError(req, 404, "unknown stage request");
262  }
263 
264  if(ExtractRequestId(resource, kReleasePrefix, requestId))
265  {
266  return Release(req, requestId, body);
267  }
268  return SendError(req, 404, "unexpected Tape REST API path");
269 }
270 
271 int TapeApiHandler::Init(const char * /*cfgfile*/)
272 {
273  const auto status = m_store.Initialize();
274  m_initError = status.message;
275  return status ? 0 : 1;
276 }
277 
278 int TapeApiHandler::Discovery(XrdHttpExtReq &req)
279 {
280  if(req.verb != "GET") return SendError(req, 405, "expected GET");
281 
282  const auto hostHeader =
284  const std::string host =
285  hostHeader == req.headers.end() ? "" : hostHeader->second;
286  if(host.empty()) return SendError(req, 400, "missing Host header");
287 
288  Json body;
289  body["sitename"] = m_siteName;
290  body["endpoints"] = Json::array({
291  {{"uri", "https://" + host + "/api/v1"}, {"version", "v1"}}
292  });
293  return SendJson(req, 200, body.dump());
294 }
295 
296 int TapeApiHandler::Stage(XrdHttpExtReq &req, const std::string &body)
297 {
298  if(req.verb != "POST") return SendError(req, 405, "expected POST");
299  Json json;
300  std::string error;
301  if(!ParseJsonBody(body, json, error)) return SendError(req, 400, error);
302  if(!json.is_object() || !json.contains("files")
303  || !json["files"].is_array() || json["files"].empty())
304  {
305  return SendError(req, 400,
306  "stage request must contain a non-empty files array");
307  }
308  for(const auto &file : json["files"])
309  {
310  if(!file.is_object() || !file.contains("path")
311  || !file["path"].is_string()
312  || file["path"].get<std::string>().empty())
313  {
314  return SendError(req, 400,
315  "stage files must contain a non-empty path");
316  }
317  }
318 
319  std::string requestId;
320  const auto status = m_store.CreateStage(json["files"], requestId);
321  if(!status) return SendError(req, status.code, status.message);
322  const std::string response = Json({{"requestId", requestId}}).dump();
323  return SendJson(req, 201, response,
324  "Location: /api/v1/stage/" + requestId);
325 }
326 
327 int TapeApiHandler::StageStatus(XrdHttpExtReq &req,
328  const std::string &requestId)
329 {
330  if(req.verb != "GET") return SendError(req, 405, "expected GET");
331  Json response;
332  const auto status = m_store.GetStage(requestId, response);
333  return SendStatus(req, status, response.dump());
334 }
335 
336 int TapeApiHandler::StageCancel(XrdHttpExtReq &req,
337  const std::string &requestId,
338  const std::string &body)
339 {
340  if(req.verb != "POST") return SendError(req, 405, "expected POST");
341  Json paths;
342  std::string error;
343  if(!ParsePaths(body, paths, error)) return SendError(req, 400, error);
344  return SendStatus(req, m_store.CancelStage(requestId, paths));
345 }
346 
347 int TapeApiHandler::StageDelete(XrdHttpExtReq &req,
348  const std::string &requestId)
349 {
350  if(req.verb != "DELETE") return SendError(req, 405, "expected DELETE");
351  return SendStatus(req, m_store.DeleteStage(requestId));
352 }
353 
354 int TapeApiHandler::Release(XrdHttpExtReq &req,
355  const std::string &requestId,
356  const std::string &body)
357 {
358  if(req.verb != "POST") return SendError(req, 405, "expected POST");
359  Json paths;
360  std::string error;
361  if(!ParsePaths(body, paths, error)) return SendError(req, 400, error);
362  return SendStatus(req, m_store.Release(requestId, paths));
363 }
364 
365 int TapeApiHandler::ArchiveInfo(XrdHttpExtReq &req,
366  const std::string &body)
367 {
368  if(req.verb != "POST") return SendError(req, 405, "expected POST");
369  Json paths;
370  std::string error;
371  if(!ParsePaths(body, paths, error)) return SendError(req, 400, error);
372  Json response;
373  const auto status = m_store.ArchiveInfo(paths, response);
374  return SendStatus(req, status, response.dump());
375 }
376 }
377 
379 
380 extern "C"
381 {
383  XrdSysError *eDest, const char *confg, const char *parms,
384  XrdOucEnv * /*myEnv*/)
385 {
386  if(!parms || !*parms)
387  {
388  eDest->Emsg("TapeApiInitialize",
389  "Tape API handler requires a local state directory");
390  return nullptr;
391  }
392 
393  std::string parameters(parms);
394  XrdOucTokenizer options(parameters.data());
395  options.GetLine();
396  const char *root = options.GetToken();
397  const char *maxRequestSizeOption = options.GetToken();
398  if(!root || !*root)
399  {
400  eDest->Emsg("TapeApiInitialize",
401  "Tape API handler requires a local state directory");
402  return nullptr;
403  }
404 
405  long long maxRequestSize = kDefaultMaxRequestSize;
406  if(maxRequestSizeOption
407  && XrdOuca2x::a2sz(*eDest, "Tape API maximum request size",
408  maxRequestSizeOption, &maxRequestSize, 1, INT_MAX))
409  {
410  return nullptr;
411  }
412  if(options.GetToken())
413  {
414  eDest->Emsg("TapeApiInitialize",
415  "Tape API handler received unexpected parameters");
416  return nullptr;
417  }
418 
419  const char *siteName = std::getenv("XRDSITE");
420  if(!siteName || !*siteName)
421  {
422  eDest->Emsg("TapeApiInitialize",
423  "all.sitename must be configured for the Tape API handler");
424  return nullptr;
425  }
426 
427  auto *handler = new TapeApiHandler(root, maxRequestSize, siteName);
428  if(handler->Init(confg) != 0)
429  {
430  eDest->Emsg("TapeApiInitialize", handler->InitializationError().c_str());
431  delete handler;
432  return nullptr;
433  }
434  return handler;
435 }
436 }
XrdSysError eDest(0, "HttpMon")
XrdVERSIONINFO(XrdHttpGetExtHandler, TapeApi)
XrdHttpExtHandler * XrdHttpGetExtHandler(XrdSysError *eDest, const char *confg, const char *parms, XrdOucEnv *)
nlohmann::json json
virtual bool MatchesPath(const char *verb, const char *path)=0
Tells if the incoming path is recognized as one of the paths that have to be processed.
virtual int ProcessReq(XrdHttpExtReq &)=0
virtual int Init(const char *cfgfile)=0
Initializes the external request handler.
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....
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 * GetToken(char **rest=0, int lowcase=0)
static int a2sz(XrdSysError &, const char *emsg, const char *item, long long *val, long long minv=-1, long long maxv=-1)
Definition: XrdOuca2x.cc:257
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
Definition: XrdSysError.cc:116