GCC Code Coverage Report


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