28#include "XrdOuc/XrdOucJson.hh"
32#include "XrdVersion.hh"
41using Json = nlohmann::json;
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;
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) {}
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; }
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,
77 static bool ParsePaths(
const std::string &body, Json &paths,
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);
92 XrdHttpTapeApiStore m_store;
93 long long m_maxRequestSize;
94 std::string m_siteName;
95 std::string m_initError;
99 const std::string &body,
100 const std::string &additionalHeaders)
102 std::string headers =
"Content-Type: application/json";
103 if(!additionalHeaders.empty())
106 headers += additionalHeaders;
110 body.c_str(), body.size());
113int TapeApiHandler::SendError(XrdHttpExtReq &req,
int code,
114 const std::string &message)
116 const std::string body = Json({{
"status", code}, {
"title", message}}).dump();
118 "Content-Type: application/problem+json", body.c_str(), body.size());
121int TapeApiHandler::SendStatus(
122 XrdHttpExtReq &req,
const XrdHttpTapeApiStore::Status &status,
123 const std::string &body)
125 return status ? SendJson(req, status.
code, body)
126 : SendError(req, status.code, status.message);
129bool TapeApiHandler::ReadBody(XrdHttpExtReq &req, std::string &body,
130 int &errorCode, std::string &error)
133 if(req.
length == 0)
return true;
137 error =
"invalid negative request length";
140 if(req.
length > m_maxRequestSize)
143 error =
"request too large";
149 char *buffer =
nullptr;
151 static_cast<int>(req.
length), &buffer,
true);
152 if(bytesRead != req.
length || buffer ==
nullptr)
155 error =
"missing or invalid request body";
158 body.assign(buffer, buffer + bytesRead);
162bool TapeApiHandler::ParseJsonBody(
const std::string &body, Json &
json,
167 json = Json::parse(body);
170 catch(
const std::exception &ex)
172 error =
"malformed JSON request: " + std::string(ex.what());
177bool TapeApiHandler::ParsePaths(
const std::string &body, Json &paths,
181 if(!ParseJsonBody(body,
json, error))
return false;
182 if(!
json.is_object() || !
json.contains(
"paths")
183 || !
json[
"paths"].is_array() ||
json[
"paths"].empty())
185 error =
"request must contain a non-empty paths array";
188 for(
const auto &item :
json[
"paths"])
190 if(!item.is_string() || item.get<std::string>().empty())
192 error =
"paths entries must be non-empty strings";
196 paths = std::move(
json[
"paths"]);
200bool TapeApiHandler::ExtractRequestId(
const std::string &resource,
201 const std::string &prefix,
202 std::string &requestId)
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;
209bool TapeApiHandler::MatchesPath(
const char * ,
const char *path)
211 if(!path)
return false;
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,
219 || resource == kArchiveInfoPath;
222int TapeApiHandler::ProcessReq(XrdHttpExtReq &req)
224 const std::string resource = req.
resource;
227 if(req.
verb ==
"POST")
231 if(!ReadBody(req, body, errorCode, error))
233 return SendError(req, errorCode, error);
237 if(resource == kDiscoveryPath)
return Discovery(req);
238 if(resource == kStagePath)
return Stage(req, body);
239 if(resource == kArchiveInfoPath)
return ArchiveInfo(req, body);
241 std::string requestId;
242 if(resource.compare(0,
sizeof(kStagePrefix) - 1, kStagePrefix) == 0)
244 if(resource.size() >
sizeof(kStageCancelSuffix) - 1
245 && resource.compare(resource.size() - (
sizeof(kStageCancelSuffix) - 1),
246 sizeof(kStageCancelSuffix) - 1,
247 kStageCancelSuffix) == 0)
249 const std::string stageResource = resource.substr(
250 0, resource.size() - (
sizeof(kStageCancelSuffix) - 1));
251 if(ExtractRequestId(stageResource, kStagePrefix, requestId))
253 return StageCancel(req, requestId, body);
256 else if(ExtractRequestId(resource, kStagePrefix, requestId))
258 return req.
verb ==
"DELETE" ? StageDelete(req, requestId)
259 : StageStatus(req, requestId);
261 return SendError(req, 404,
"unknown stage request");
264 if(ExtractRequestId(resource, kReleasePrefix, requestId))
266 return Release(req, requestId, body);
268 return SendError(req, 404,
"unexpected Tape REST API path");
271int TapeApiHandler::Init(
const char * )
275 return status ? 0 : 1;
278int TapeApiHandler::Discovery(XrdHttpExtReq &req)
280 if(req.
verb !=
"GET")
return SendError(req, 405,
"expected GET");
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");
289 body[
"sitename"] = m_siteName;
290 body[
"endpoints"] = Json::array({
291 {{
"uri",
"https://" + host +
"/api/v1"}, {
"version",
"v1"}}
293 return SendJson(req, 200, body.dump());
296int TapeApiHandler::Stage(XrdHttpExtReq &req,
const std::string &body)
298 if(req.
verb !=
"POST")
return SendError(req, 405,
"expected POST");
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())
305 return SendError(req, 400,
306 "stage request must contain a non-empty files array");
308 for(
const auto &file :
json[
"files"])
310 if(!file.is_object() || !file.contains(
"path")
311 || !file[
"path"].is_string()
312 || file[
"path"].get<std::string>().empty())
314 return SendError(req, 400,
315 "stage files must contain a non-empty path");
319 std::string 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);
327int TapeApiHandler::StageStatus(XrdHttpExtReq &req,
328 const std::string &requestId)
330 if(req.
verb !=
"GET")
return SendError(req, 405,
"expected GET");
332 const auto status = m_store.
GetStage(requestId, response);
333 return SendStatus(req, status, response.dump());
336int TapeApiHandler::StageCancel(XrdHttpExtReq &req,
337 const std::string &requestId,
338 const std::string &body)
340 if(req.
verb !=
"POST")
return SendError(req, 405,
"expected POST");
343 if(!ParsePaths(body, paths, error))
return SendError(req, 400, error);
344 return SendStatus(req, m_store.
CancelStage(requestId, paths));
347int TapeApiHandler::StageDelete(XrdHttpExtReq &req,
348 const std::string &requestId)
350 if(req.
verb !=
"DELETE")
return SendError(req, 405,
"expected DELETE");
351 return SendStatus(req, m_store.
DeleteStage(requestId));
354int TapeApiHandler::Release(XrdHttpExtReq &req,
355 const std::string &requestId,
356 const std::string &body)
358 if(req.
verb !=
"POST")
return SendError(req, 405,
"expected POST");
361 if(!ParsePaths(body, paths, error))
return SendError(req, 400, error);
362 return SendStatus(req, m_store.
Release(requestId, paths));
365int TapeApiHandler::ArchiveInfo(XrdHttpExtReq &req,
366 const std::string &body)
368 if(req.
verb !=
"POST")
return SendError(req, 405,
"expected POST");
371 if(!ParsePaths(body, paths, error))
return SendError(req, 400, error);
373 const auto status = m_store.
ArchiveInfo(paths, response);
374 return SendStatus(req, status, response.dump());
386 if(!parms || !*parms)
389 "Tape API handler requires a local state directory");
393 std::string parameters(parms);
396 const char *root = options.
GetToken();
397 const char *maxRequestSizeOption = options.
GetToken();
401 "Tape API handler requires a local state directory");
405 long long maxRequestSize = kDefaultMaxRequestSize;
406 if(maxRequestSizeOption
408 maxRequestSizeOption, &maxRequestSize, 1, INT_MAX))
415 "Tape API handler received unexpected parameters");
419 const char *siteName = std::getenv(
"XRDSITE");
420 if(!siteName || !*siteName)
423 "all.sitename must be configured for the Tape API handler");
427 auto *handler =
new TapeApiHandler(root, maxRequestSize, siteName);
428 if(handler->Init(confg) != 0)
430 eDest->
Emsg(
"TapeApiInitialize", handler->InitializationError().c_str());
XrdVERSIONINFO(XrdClGetPlugIn, XrdClGetPlugIn) extern "C"
XrdHttpExtHandler * XrdHttpGetExtHandler(XrdHttpExtHandlerArgs)
XrdSysError eDest(0, "HttpMon")
std::map< std::string, std::string > & headers
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)
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)