GCC Code Coverage Report


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