GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/publish/repository_session.cc
Date: 2026-08-30 02:40:36
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