GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/publish/repository_session.cc
Date: 2025-06-22 02:36:02
Exec Total Coverage
Lines: 0 190 0.0%
Branches: 0 345 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->string_value;
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->string_value);
176 *session_token = token->string_value;
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(
183 kLogCvmfs, llvl | kLogStdout, "Path busy. Time remaining = %s",
184 (time_remaining != NULL) ? time_remaining->string_value : "UNKNOWN");
185 return kLeaseReplyBusy;
186 } else if (status == "error") {
187 const JSON *reason = JsonDocument::SearchInObject(reply->root(), "reason",
188 JSON_STRING);
189 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Error: '%s'",
190 (reason != NULL) ? reason->string_value : "");
191 } else {
192 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Unknown reply. Status: %s",
193 status.c_str());
194 }
195 }
196
197 return kLeaseReplyFailure;
198 }
199
200
201 static LeaseReply ParseDropReply(const CurlBuffer &buffer, int llvl) {
202 if (buffer.data.size() == 0) {
203 return kLeaseReplyFailure;
204 }
205
206 const UniquePtr<const JsonDocument> reply(JsonDocument::Create(buffer.data));
207 if (!reply.IsValid() || !reply->IsValid()) {
208 return kLeaseReplyFailure;
209 }
210
211 const JSON *result = JsonDocument::SearchInObject(reply->root(), "status",
212 JSON_STRING);
213 if (result != NULL) {
214 const std::string status = result->string_value;
215 if (status == "ok") {
216 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Gateway reply: ok");
217 return kLeaseReplySuccess;
218 } else if (status == "invalid_token") {
219 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Error: invalid session token");
220 } else if (status == "error") {
221 const JSON *reason = JsonDocument::SearchInObject(reply->root(), "reason",
222 JSON_STRING);
223 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Error from gateway: '%s'",
224 (reason != NULL) ? reason->string_value : "");
225 } else {
226 LogCvmfs(kLogCvmfs, llvl | kLogStdout, "Unknown reply. Status: %s",
227 status.c_str());
228 }
229 }
230
231 return kLeaseReplyFailure;
232 }
233
234 } // anonymous namespace
235
236 namespace publish {
237
238 Publisher::Session::Session(const Settings &settings_session)
239 : settings_(settings_session)
240 , keep_alive_(false)
241 // TODO(jblomer): it would be better to actually read & validate the token
242 , has_lease_(FileExists(settings_.token_path)) { }
243
244
245 Publisher::Session::Session(const SettingsPublisher &settings_publisher,
246 int llvl) {
247 keep_alive_ = false;
248 if (settings_publisher.storage().type()
249 != upload::SpoolerDefinition::Gateway) {
250 has_lease_ = true;
251 return;
252 }
253
254 settings_.service_endpoint = settings_publisher.storage().endpoint();
255 settings_.repo_path = settings_publisher.fqrn() + "/"
256 + settings_publisher.transaction().lease_path();
257 settings_.gw_key_path = settings_publisher.keychain().gw_key_path();
258 settings_.token_path = settings_publisher.transaction()
259 .spool_area()
260 .gw_session_token();
261 settings_.llvl = llvl;
262
263 // TODO(jblomer): it would be better to actually read & validate the token
264 has_lease_ = FileExists(settings_.token_path);
265 // If a lease is already present, we don't want to remove it automatically
266 keep_alive_ = has_lease_;
267 }
268
269
270 void Publisher::Session::SetKeepAlive(bool value) { keep_alive_ = value; }
271
272
273 void Publisher::Session::Acquire() {
274 if (has_lease_)
275 return;
276
277 const gateway::GatewayKey gw_key =
278 gateway::ReadGatewayKey(settings_.gw_key_path);
279 if (!gw_key.IsValid()) {
280 throw EPublish("cannot read gateway key: " + settings_.gw_key_path,
281 EPublish::kFailGatewayKey);
282 }
283 CurlBuffer buffer;
284 MakeAcquireRequest(gw_key, settings_.repo_path, settings_.service_endpoint,
285 settings_.llvl, &buffer);
286
287 std::string session_token;
288 const LeaseReply rep =
289 ParseAcquireReply(buffer, &session_token, settings_.llvl);
290 switch (rep) {
291 case kLeaseReplySuccess: {
292 has_lease_ = true;
293 const bool rvb =
294 SafeWriteToFile(session_token, settings_.token_path, 0600);
295 if (!rvb) {
296 throw EPublish("cannot write session token: " + settings_.token_path);
297 }
298 } break;
299 case kLeaseReplyBusy:
300 throw EPublish("lease path busy", EPublish::kFailLeaseBusy);
301 break;
302 case kLeaseReplyFailure:
303 default:
304 throw EPublish("cannot parse session token", EPublish::kFailLeaseBody);
305 }
306 }
307
308 void Publisher::Session::Drop() {
309 if (!has_lease_)
310 return;
311 // TODO(jblomer): there might be a better way to distinguish between the
312 // nop-session and a real session
313 if (settings_.service_endpoint.empty())
314 return;
315
316 std::string token;
317 const int fd_token = open(settings_.token_path.c_str(), O_RDONLY);
318 const bool rvb = SafeReadToString(fd_token, &token);
319 close(fd_token);
320 if (!rvb) {
321 throw EPublish("cannot read session token: " + settings_.token_path,
322 EPublish::kFailGatewayKey);
323 }
324 const gateway::GatewayKey gw_key =
325 gateway::ReadGatewayKey(settings_.gw_key_path);
326 if (!gw_key.IsValid()) {
327 throw EPublish("cannot read gateway key: " + settings_.gw_key_path,
328 EPublish::kFailGatewayKey);
329 }
330
331 CurlBuffer buffer;
332 MakeDropRequest(gw_key, token, settings_.service_endpoint, settings_.llvl,
333 &buffer);
334 const LeaseReply rep = ParseDropReply(buffer, settings_.llvl);
335 int rvi = 0;
336 switch (rep) {
337 case kLeaseReplySuccess:
338 has_lease_ = false;
339 rvi = unlink(settings_.token_path.c_str());
340 if (rvi != 0)
341 throw EPublish("cannot delete session token " + settings_.token_path);
342 break;
343 case kLeaseReplyFailure:
344 default:
345 throw EPublish("gateway doesn't recognize the lease or cannot drop it",
346 EPublish::kFailLeaseBody);
347 }
348 }
349
350 Publisher::Session::~Session() {
351 if (keep_alive_)
352 return;
353
354 Drop();
355 }
356
357 } // namespace publish
358