GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/publish/repository_session.cc
Date: 2026-04-05 02:35:23
Exec Total Coverage
Lines: 0 193 0.0%
Branches: 0 383 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 */
4
5
6 #include <fcntl.h>
7 #include <unistd.h>
8
9 #include <cassert>
10 #include <string>
11
12 #include "backoff.h"
13 #include "catalog_mgr_ro.h"
14 #include "crypto/hash.h"
15 #include "directory_entry.h"
16 #include "duplex_curl.h"
17 #include "gateway_util.h"
18 #include "json_document.h"
19 #include "publish/except.h"
20 #include "publish/repository.h"
21 #include "ssl.h"
22 #include "upload.h"
23 #include "util/logging.h"
24 #include "util/pointer.h"
25 #include "util/posix.h"
26 #include "util/string.h"
27
28 namespace {
29
30 struct CurlBuffer {
31 std::string data;
32 };
33
34 enum LeaseReply {
35 kLeaseReplySuccess,
36 kLeaseReplyBusy,
37 kLeaseReplyFailure
38 };
39
40 static CURL *PrepareCurl(const std::string &method) {
41 const char *user_agent_string = "cvmfs/" CVMFS_VERSION;
42
43 CURL *h_curl = curl_easy_init();
44 assert(h_curl != NULL);
45
46 curl_easy_setopt(h_curl, CURLOPT_NOPROGRESS, 1L);
47 curl_easy_setopt(h_curl, CURLOPT_USERAGENT, user_agent_string);
48 curl_easy_setopt(h_curl, CURLOPT_MAXREDIRS, 50L);
49 curl_easy_setopt(h_curl, CURLOPT_CUSTOMREQUEST, method.c_str());
50
51 return h_curl;
52 }
53
54 static size_t RecvCB(void *buffer, size_t size, size_t nmemb, void *userp) {
55 CurlBuffer *my_buffer = static_cast<CurlBuffer *>(userp);
56
57 if (size * nmemb < 1) {
58 return 0;
59 }
60
61 my_buffer->data = static_cast<char *>(buffer);
62
63 return my_buffer->data.size();
64 }
65
66 static void MakeAcquireRequest(const gateway::GatewayKey &key,
67 const std::string &repo_path,
68 const std::string &repo_service_url,
69 int llvl,
70 CurlBuffer *buffer) {
71 CURLcode ret = static_cast<CURLcode>(0);
72
73 CURL *h_curl = PrepareCurl("POST");
74
75 const std::string payload = "{\"path\" : \"" + repo_path
76 + "\", \"api_version\" : \""
77 + StringifyInt(gateway::APIVersion()) + "\", "
78 + "\"hostname\" : \"" + GetHostname() + "\"}";
79
80 shash::Any hmac(shash::kSha1);
81 shash::HmacString(key.secret(), payload, &hmac);
82 SslCertificateStore cs;
83 cs.UseSystemCertificatePath();
84 cs.ApplySslCertificatePath(h_curl);
85
86 const std::string header_str = std::string("Authorization: ") + key.id() + " "
87 + Base64(hmac.ToString(false));
88 struct curl_slist *auth_header = NULL;
89 auth_header = curl_slist_append(auth_header, header_str.c_str());
90 curl_easy_setopt(h_curl, CURLOPT_HTTPHEADER, auth_header);
91
92 // Make request to acquire lease from repo services
93 curl_easy_setopt(h_curl, CURLOPT_URL, (repo_service_url + "/leases").c_str());
94 curl_easy_setopt(h_curl, CURLOPT_POSTFIELDSIZE_LARGE,
95 static_cast<curl_off_t>(payload.length()));
96 curl_easy_setopt(h_curl, CURLOPT_POSTFIELDS, payload.c_str());
97 curl_easy_setopt(h_curl, CURLOPT_WRITEFUNCTION, RecvCB);
98 curl_easy_setopt(h_curl, CURLOPT_WRITEDATA, buffer);
99
100 ret = curl_easy_perform(h_curl);
101 curl_easy_cleanup(h_curl);
102 if (ret != CURLE_OK) {
103 LogCvmfs(kLogUploadGateway, llvl | kLogStderr,
104 "Make lease acquire request failed: %d. Reply: %s", ret,
105 buffer->data.c_str());
106 throw publish::EPublish("cannot acquire lease",
107 publish::EPublish::kFailLeaseHttp);
108 }
109 }
110
111 // TODO(jblomer): This should eventually also handle the POST request for
112 // committing a transaction
113 static void MakeDropRequest(const gateway::GatewayKey &key,
114 const std::string &session_token,
115 const std::string &repo_service_url,
116 int llvl,
117 CurlBuffer *reply) {
118 CURLcode ret = static_cast<CURLcode>(0);
119
120 CURL *h_curl = PrepareCurl("DELETE");
121
122 shash::Any hmac(shash::kSha1);
123 shash::HmacString(key.secret(), session_token, &hmac);
124 SslCertificateStore cs;
125 cs.UseSystemCertificatePath();
126 cs.ApplySslCertificatePath(h_curl);
127
128 const std::string header_str = std::string("Authorization: ") + key.id() + " "
129 + Base64(hmac.ToString(false));
130 struct curl_slist *auth_header = NULL;
131 auth_header = curl_slist_append(auth_header, header_str.c_str());
132 curl_easy_setopt(h_curl, CURLOPT_HTTPHEADER, auth_header);
133
134 curl_easy_setopt(h_curl, CURLOPT_URL,
135 (repo_service_url + "/leases/" + session_token).c_str());
136 curl_easy_setopt(h_curl, CURLOPT_POSTFIELDSIZE_LARGE,
137 static_cast<curl_off_t>(0));
138 curl_easy_setopt(h_curl, CURLOPT_POSTFIELDS, NULL);
139 curl_easy_setopt(h_curl, CURLOPT_WRITEFUNCTION, RecvCB);
140 curl_easy_setopt(h_curl, CURLOPT_WRITEDATA, reply);
141
142 ret = curl_easy_perform(h_curl);
143 curl_easy_cleanup(h_curl);
144 if (ret != CURLE_OK) {
145 LogCvmfs(kLogUploadGateway, llvl | kLogStderr,
146 "Make lease drop request failed: %d. Reply: '%s'", ret,
147 reply->data.c_str());
148 throw publish::EPublish("cannot drop lease",
149 publish::EPublish::kFailLeaseHttp);
150 }
151 }
152
153 static LeaseReply ParseAcquireReply(const CurlBuffer &buffer,
154 std::string *session_token,
155 int llvl) {
156 if (buffer.data.size() == 0 || session_token == NULL) {
157 return kLeaseReplyFailure;
158 }
159
160 const UniquePtr<JsonDocument> reply(JsonDocument::Create(buffer.data));
161 if (!reply.IsValid() || !reply->IsValid()) {
162 return kLeaseReplyFailure;
163 }
164
165 const JSON *result = JsonDocument::SearchInObject(reply->root(), "status",
166 JSON_STRING);
167 if (result != NULL) {
168 const std::string status = result->get<std::string>();
169 if (status == "ok") {
170 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Gateway reply: ok");
171 const JSON *token = JsonDocument::SearchInObject(
172 reply->root(), "session_token", JSON_STRING);
173 if (token != NULL) {
174 LogCvmfs(kLogCvmfs, kLogDebug, "Session token: %s",
175 token->get<std::string>().c_str());
176 *session_token = token->get<std::string>();
177 return kLeaseReplySuccess;
178 }
179 } else if (status == "path_busy") {
180 const JSON *time_remaining = JsonDocument::SearchInObject(
181 reply->root(), "time_remaining", JSON_STRING);
182 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Path busy. Time remaining = %s",
183 (time_remaining != NULL)
184 ? time_remaining->get<std::string>().c_str()
185 : "UNKNOWN");
186 return kLeaseReplyBusy;
187 } else if (status == "error") {
188 const JSON *reason = JsonDocument::SearchInObject(reply->root(), "reason",
189 JSON_STRING);
190 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Error: '%s'",
191 (reason != NULL) ? reason->get<std::string>().c_str() : "");
192 } else {
193 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Unknown reply. Status: %s",
194 status.c_str());
195 }
196 }
197
198 return kLeaseReplyFailure;
199 }
200
201
202 static LeaseReply ParseDropReply(const CurlBuffer &buffer, int llvl) {
203 if (buffer.data.size() == 0) {
204 return kLeaseReplyFailure;
205 }
206
207 const UniquePtr<const JsonDocument> reply(JsonDocument::Create(buffer.data));
208 if (!reply.IsValid() || !reply->IsValid()) {
209 return kLeaseReplyFailure;
210 }
211
212 const JSON *result = JsonDocument::SearchInObject(reply->root(), "status",
213 JSON_STRING);
214 if (result != NULL) {
215 const std::string status = result->get<std::string>();
216 if (status == "ok") {
217 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Gateway reply: ok");
218 return kLeaseReplySuccess;
219 } else if (status == "invalid_token") {
220 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Error: invalid session token");
221 } else if (status == "error") {
222 const JSON *reason = JsonDocument::SearchInObject(reply->root(), "reason",
223 JSON_STRING);
224 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Error from gateway: '%s'",
225 (reason != NULL) ? reason->get<std::string>().c_str() : "");
226 } else {
227 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Unknown reply. Status: %s",
228 status.c_str());
229 }
230 }
231
232 return kLeaseReplyFailure;
233 }
234
235 } // anonymous namespace
236
237 namespace publish {
238
239 Publisher::Session::Session(const Settings &settings_session)
240 : settings_(settings_session)
241 , keep_alive_(false)
242 // TODO(jblomer): it would be better to actually read & validate the token
243 , has_lease_(FileExists(settings_.token_path)) { }
244
245
246 Publisher::Session::Session(const SettingsPublisher &settings_publisher,
247 int llvl) {
248 keep_alive_ = false;
249 if (settings_publisher.storage().type()
250 != upload::SpoolerDefinition::Gateway) {
251 has_lease_ = true;
252 return;
253 }
254
255 settings_.service_endpoint = settings_publisher.storage().endpoint();
256 settings_.repo_path = settings_publisher.fqrn() + "/"
257 + settings_publisher.transaction().lease_path();
258 settings_.gw_key_path = settings_publisher.keychain().gw_key_path();
259 settings_.token_path = settings_publisher.transaction()
260 .spool_area()
261 .gw_session_token();
262 settings_.llvl = llvl;
263
264 // TODO(jblomer): it would be better to actually read & validate the token
265 has_lease_ = FileExists(settings_.token_path);
266 // If a lease is already present, we don't want to remove it automatically
267 keep_alive_ = has_lease_;
268 }
269
270
271 void Publisher::Session::SetKeepAlive(bool value) { keep_alive_ = value; }
272
273
274 void Publisher::Session::Acquire() {
275 if (has_lease_)
276 return;
277
278 const gateway::GatewayKey gw_key = gateway::ReadGatewayKey(
279 settings_.gw_key_path);
280 if (!gw_key.IsValid()) {
281 throw EPublish("cannot read gateway key: " + settings_.gw_key_path,
282 EPublish::kFailGatewayKey);
283 }
284 CurlBuffer buffer;
285 MakeAcquireRequest(gw_key, settings_.repo_path, settings_.service_endpoint,
286 settings_.llvl, &buffer);
287
288 std::string session_token;
289 const LeaseReply rep = ParseAcquireReply(buffer, &session_token,
290 settings_.llvl);
291 switch (rep) {
292 case kLeaseReplySuccess: {
293 has_lease_ = true;
294 const bool rvb = SafeWriteToFile(session_token, settings_.token_path,
295 0600);
296 if (!rvb) {
297 throw EPublish("cannot write session token: " + settings_.token_path);
298 }
299 } break;
300 case kLeaseReplyBusy:
301 throw EPublish("lease path busy", EPublish::kFailLeaseBusy);
302 break;
303 case kLeaseReplyFailure:
304 default:
305 throw EPublish("cannot parse session token", EPublish::kFailLeaseBody);
306 }
307 }
308
309 void Publisher::Session::Drop() {
310 if (!has_lease_)
311 return;
312 // TODO(jblomer): there might be a better way to distinguish between the
313 // nop-session and a real session
314 if (settings_.service_endpoint.empty())
315 return;
316
317 std::string token;
318 const int fd_token = open(settings_.token_path.c_str(), O_RDONLY);
319 const bool rvb = SafeReadToString(fd_token, &token);
320 close(fd_token);
321 if (!rvb) {
322 throw EPublish("cannot read session token: " + settings_.token_path,
323 EPublish::kFailGatewayKey);
324 }
325 const gateway::GatewayKey gw_key = gateway::ReadGatewayKey(
326 settings_.gw_key_path);
327 if (!gw_key.IsValid()) {
328 throw EPublish("cannot read gateway key: " + settings_.gw_key_path,
329 EPublish::kFailGatewayKey);
330 }
331
332 CurlBuffer buffer;
333 MakeDropRequest(gw_key, token, settings_.service_endpoint, settings_.llvl,
334 &buffer);
335 const LeaseReply rep = ParseDropReply(buffer, settings_.llvl);
336 int rvi = 0;
337 switch (rep) {
338 case kLeaseReplySuccess:
339 has_lease_ = false;
340 rvi = unlink(settings_.token_path.c_str());
341 if (rvi != 0)
342 throw EPublish("cannot delete session token " + settings_.token_path);
343 break;
344 case kLeaseReplyFailure:
345 default:
346 throw EPublish("gateway doesn't recognize the lease or cannot drop it",
347 EPublish::kFailLeaseBody);
348 }
349 }
350
351 Publisher::Session::~Session() {
352 if (keep_alive_)
353 return;
354
355 Drop();
356 }
357
358 } // namespace publish
359