XRootD
Loading...
Searching...
No Matches
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
27#include "XrdOuc/XrdOuca2x.hh"
28#include "XrdOuc/XrdOucJson.hh"
31#include "XrdSys/XrdSysError.hh"
32#include "XrdVersion.hh"
33
34#include <climits>
35#include <cstdlib>
36#include <exception>
37#include <string>
38
39namespace
40{
41using Json = nlohmann::json;
42
43constexpr char kDiscoveryPath[] = "/.well-known/wlcg-tape-rest-api";
44constexpr char kStagePath[] = "/api/v1/stage";
45constexpr char kStagePrefix[] = "/api/v1/stage/";
46constexpr char kStageCancelSuffix[] = "/cancel";
47constexpr char kReleasePrefix[] = "/api/v1/release/";
48constexpr char kArchiveInfoPath[] = "/api/v1/archiveinfo";
49constexpr long long kDefaultMaxRequestSize = 4 * 1024 * 1024;
50
51class 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
98int 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
113int 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
121int 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
129bool 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
162bool 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
177bool 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
200bool 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
209bool 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
222int 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
271int 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
278int 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
296int 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
327int 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
336int 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
347int 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
354int 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
365int 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
380extern "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}
XrdVERSIONINFO(XrdClGetPlugIn, XrdClGetPlugIn) extern "C"
XrdHttpExtHandler * XrdHttpGetExtHandler(XrdHttpExtHandlerArgs)
XrdSysError eDest(0, "HttpMon")
nlohmann::json json
std::map< std::string, std::string > & headers
std::string resource
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.
Status DeleteStage(const std::string &requestId)
Status GetStage(const std::string &requestId, nlohmann::json &response)
Status CancelStage(const std::string &requestId, const nlohmann::json &paths)
Status CreateStage(const nlohmann::json &files, std::string &requestId)
Status ArchiveInfo(const nlohmann::json &paths, nlohmann::json &response)
Status Release(const std::string &requestId, const nlohmann::json &paths)
static std::map< std::string, T >::const_iterator caseInsensitiveFind(const std::map< std::string, T > &m, const std::string &lowerCaseSearchKey)
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)