GCC Code Coverage Report


Directory: ./
File: CurlManager.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 248 365 67.9%
Functions: 26 37 70.3%
Branches: 223 554 40.3%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2024 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include <algorithm>
16 #include <atomic>
17 #include <chrono>
18 #include <cstring>
19 #include <list>
20 #include <memory>
21 #include <mutex>
22 #include <string>
23 #include <thread>
24 #include <vector>
25
26 #include "CurlManager.h"
27 #include "CurlSocket.h"
28 #include "fppversion.h"
29 #include "log.h"
30
31 CurlManager CurlManager::INSTANCE;
32
33 2346 CurlManager::CurlManager() {
34 2346 }
35
36 458 CurlManager::~CurlManager() {
37
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 if (curlMulti) {
38 ✗ curl_multi_cleanup(curlMulti);
39 }
40 // No handle should still reference these shares by the time the singleton is
41 // torn down at process exit; free whatever cookie sessions were left behind.
42
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
458 for (auto& p : cookieSessions) {
43 ✗ if (p.second->share) {
44 ✗ curl_share_cleanup(p.second->share);
45 }
46 ✗ delete p.second;
47 }
48 458 cookieSessions.clear();
49 458 }
50
51 // curl calls these around every access to shared data (only COOKIE is shared
52 // here). The userdata is the session's own std::mutex; a single mutex is
53 // sufficient because only one data class is shared, so curl never nests locks.
54 68 static void curlShareLockCB(CURL* handle, curl_lock_data data, curl_lock_access access, void* userptr) {
55 68 static_cast<std::mutex*>(userptr)->lock();
56 68 }
57 68 static void curlShareUnlockCB(CURL* handle, curl_lock_data data, void* userptr) {
58 68 static_cast<std::mutex*>(userptr)->unlock();
59 68 }
60
61 12 CURLSH* CurlManager::getCookieShare(const std::string& token) {
62
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::unique_lock<std::mutex> l(lock);
63
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 CookieSession*& cs = cookieSessions[token];
64
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 7 times.
12 if (cs == nullptr) {
65
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
5 cs = new CookieSession();
66
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 cs->share = curl_share_init();
67
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 curl_share_setopt(cs->share, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
68
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 curl_share_setopt(cs->share, CURLSHOPT_LOCKFUNC, curlShareLockCB);
69
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 curl_share_setopt(cs->share, CURLSHOPT_UNLOCKFUNC, curlShareUnlockCB);
70
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 curl_share_setopt(cs->share, CURLSHOPT_USERDATA, &cs->mutex);
71 }
72 12 return cs->share;
73 12 }
74
75 15 void CurlManager::releaseCookieSession(const std::string& token) {
76 15 CookieSession* cs = nullptr;
77 {
78
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 std::unique_lock<std::mutex> l(lock);
79
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 auto it = cookieSessions.find(token);
80
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
15 if (it == cookieSessions.end()) {
81 10 return;
82 }
83 5 cs = it->second;
84
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 cookieSessions.erase(it);
85
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 10 times.
15 }
86 // Caller contract: no easy handle still has CURLOPT_SHARE pointing at this
87 // share (the owner's requests have completed or been cancelled). If curl
88 // still sees it in use we must NOT free it - the share holds &cs->mutex, so
89 // deleting cs would leave curl calling into a freed mutex. Leak + shout.
90 5 CURLSHcode sc = curl_share_cleanup(cs->share);
91
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (sc == CURLSHE_IN_USE) {
92 ✗ LogErr(VB_GENERAL, "CurlManager cookie session '%s' still in use at release; leaking share to avoid a UAF\n", token.c_str());
93 ✗ return;
94 }
95
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 delete cs;
96 }
97
98 1342 void CurlManager::addCURL(const std::string& furl, CURL* curl, std::function<void(CURL*)>&& callback, bool autoCleanCurl,
99 const std::string& owner) {
100
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 std::unique_lock<std::mutex> l(lock);
101
2/2
✓ Branch 0 taken 136 times.
✓ Branch 1 taken 1206 times.
1342 if (curlMulti == nullptr) {
102
1/2
✓ Branch 0 taken 136 times.
✗ Branch 1 not taken.
136 curlMulti = curl_multi_init();
103
1/2
✓ Branch 0 taken 136 times.
✗ Branch 1 not taken.
136 curl_multi_setopt(curlMulti, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
104 }
105
106
2/4
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1342 times.
1342 CurlInfo* i = new CurlInfo;
107
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 i->host = getHost(furl);
108
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 i->url = furl;
109
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 i->callback = callback;
110 1342 i->curl = curl;
111 1342 i->cleanCurl = autoCleanCurl;
112
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 i->owner = owner;
113
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_multi_add_handle(curlMulti, curl);
114 1342 numCurls++;
115
2/2
✓ Branch 0 taken 7379 times.
✓ Branch 1 taken 324 times.
7703 for (int x = 0; x < curls.size(); x++) {
116
2/2
✓ Branch 0 taken 1018 times.
✓ Branch 1 taken 6361 times.
7379 if (curls[x] == nullptr) {
117 1018 curls[x] = i;
118 1018 return;
119 }
120 }
121
1/2
✓ Branch 0 taken 324 times.
✗ Branch 1 not taken.
324 curls.push_back(i);
122
2/2
✓ Branch 0 taken 324 times.
✓ Branch 1 taken 1018 times.
1342 }
123
124 3405 static size_t urlWriteData(void* buffer, size_t size, size_t nmemb, void* userp) {
125 3405 std::vector<uint8_t>* str = (std::vector<uint8_t>*)userp;
126 3405 size_t pos = str->size();
127 3405 str->resize(str->size() + size * nmemb);
128 3405 memcpy(&str->data()[pos], buffer, size * nmemb);
129 3405 return size * nmemb;
130 }
131
132 // The requests FPP makes itself (add*(), do*()) and its discovery probes hold
133 // the whole response in memory, and discovery reaches devices that are merely
134 // on the LAN: one that streamed an endless body grew fppd until the OOM killer
135 // stepped in (the timeout alone allows ~100MB at LAN speed). Returning short
136 // aborts the transfer with CURLE_WRITE_ERROR -- but the HTTP status already
137 // received still reads 200, so the request paths below report that result,
138 // like any other failed transfer, as a failure themselves (failedTransfer()).
139 // A plugin's own handle from createCurl() keeps no
140 // limit: it reads the status and the body itself, and a cut-off body with a
141 // 200 is what its proxy then served (limitResponseSize() opts a handle in).
142 static constexpr size_t MAX_CURL_RESPONSE = 16 * 1024 * 1024;
143
144 3407 static size_t urlWriteDataLimited(void* buffer, size_t size, size_t nmemb, void* userp) {
145 3407 std::vector<uint8_t>* str = (std::vector<uint8_t>*)userp;
146
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3405 times.
3407 if (size * nmemb > MAX_CURL_RESPONSE - std::min(str->size(), MAX_CURL_RESPONSE)) {
147 2 return 0;
148 }
149 3405 return urlWriteData(buffer, size, nmemb, userp);
150 }
151
152 1342 void CurlManager::limitResponseSize(CURL* c) {
153 1342 curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, urlWriteDataLimited);
154 1342 }
155
156 // The result of the transfer whose callback is running on this thread, set by
157 // doProcessCurls() around it: the callbacks take only the handle, and the
158 // result isn't one of its infos.
159 static thread_local CURLcode t_completedResult = CURLE_OK;
160
161 // Why a transfer failed, or "" if it completed. CURLINFO_RESPONSE_CODE is
162 // the last status received even when the transfer then failed: a body cut
163 // off by the size cap, a timeout or a reset partway through, or a server
164 // that closed before its Content-Length all read as a 200 with part of the
165 // body. So the requests below go by the transfer's result, not the status.
166 969 static std::string failedTransfer(CURLcode result, const CurlManager::CurlPrivateData* data) {
167
2/2
✓ Branch 0 taken 922 times.
✓ Branch 1 taken 47 times.
969 if (result == CURLE_OK) {
168
1/2
✓ Branch 0 taken 922 times.
✗ Branch 1 not taken.
1844 return "";
169 }
170
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 45 times.
47 if (result == CURLE_WRITE_ERROR) {
171 // only urlWriteDataLimited() returns short on these handles
172
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 return "the response was larger than " + std::to_string(MAX_CURL_RESPONSE / (1024 * 1024)) + " MB";
173 }
174
2/4
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
45 if (data && data->errorResp[0]) {
175
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
90 return data->errorResp;
176 }
177 ✗ return curl_easy_strerror(result);
178 }
179 ✗ static size_t urlReadData(void* ptr, size_t size, size_t nmemb, void* userp) {
180 ✗ size_t buffer_size = size * nmemb;
181 ✗ CurlManager::CurlPrivateData* dt = (CurlManager::CurlPrivateData*)userp;
182 ✗ size_t numb = dt->req->size() - dt->curPos;
183 ✗ if (numb > buffer_size) {
184 ✗ numb = buffer_size;
185 }
186 ✗ if (numb > 0) {
187 ✗ memcpy(ptr, &(*dt->req)[dt->curPos], numb);
188 }
189 ✗ dt->curPos += numb;
190 ✗ return numb;
191 }
192 1342 CURL* CurlManager::createCurl(const std::string& fullUrl, CurlPrivateData** cpd, bool upload, CURLSH* share) {
193
6/12
✓ Branch 0 taken 136 times.
✓ Branch 1 taken 1206 times.
✓ Branch 2 taken 136 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 136 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 136 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 136 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
1614 static std::string USERAGENT = std::string("FPP/") + getFPPVersionTriplet();
194
195
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 const std::string host = getHost(fullUrl);
196 // Under the lock: requests are created from API and command threads as
197 // well as the main loop, and getHostData() inserts into the map. Copy the
198 // credentials out (curl copies string options anyway).
199 1342 std::string username;
200 1342 std::string password;
201 {
202
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 std::unique_lock<std::mutex> l(lock);
203
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 HostData* hd = getHostData(host);
204
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 username = hd->username;
205
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 password = hd->password;
206 1342 }
207
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 CURL* c = curl_easy_init();
208
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 SetCurlCloseOnExec(c);
209
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_URL, fullUrl.c_str());
210
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1330 times.
1342 if (share) {
211 // Join the cookie session: arm the cookie engine (empty COOKIEFILE) and
212 // point at the shared jar so cookies persist across handles in the group.
213
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(c, CURLOPT_COOKIEFILE, "");
214
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 curl_easy_setopt(c, CURLOPT_SHARE, share);
215 }
216
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_USERAGENT, USERAGENT.c_str());
217
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
218
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, 10000L);
219
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_ACCEPT_ENCODING, "");
220
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1);
221 //curl_easy_setopt(c, CURLOPT_VERBOSE, 2L);
222
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_UPKEEP_INTERVAL_MS, 5000L);
223
224
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, urlWriteData);
225
2/4
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1342 times.
1342 CurlPrivateData* data = new CurlPrivateData();
226
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 data->host = host;
227
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_ERRORBUFFER, data->errorResp);
228
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_WRITEDATA, &data->resp);
229
1/2
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
1342 curl_easy_setopt(c, CURLOPT_PRIVATE, data);
230
2/4
✓ Branch 0 taken 1342 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1342 times.
1342 if (username != "") {
231 ✗ curl_easy_setopt(c, CURLOPT_USERNAME, username.c_str());
232 ✗ curl_easy_setopt(c, CURLOPT_PASSWORD, password.c_str());
233 ✗ curl_easy_setopt(c, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST | CURLAUTH_NEGOTIATE);
234 }
235
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1342 times.
1342 if (upload) {
236 ✗ data->req = new std::vector<uint8_t>();
237 ✗ curl_easy_setopt(c, CURLOPT_READDATA, data);
238 ✗ curl_easy_setopt(c, CURLOPT_READFUNCTION, urlReadData);
239 }
240
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1342 times.
1342 if (cpd) {
241 ✗ *cpd = data;
242 }
243 1342 return c;
244 1342 }
245
246 387 void CurlManager::add(const std::string& furl, const std::string& method, const std::string& data,
247 const std::list<std::string>& extraHeaders,
248 std::function<void(int rc, const std::string& resp)>&& callback,
249 const std::string& owner, const std::string& cookieSession) {
250
3/4
✓ Branch 0 taken 375 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
387 CURLSH* share = cookieSession.empty() ? nullptr : getCookieShare(cookieSession);
251
1/2
✓ Branch 0 taken 387 times.
✗ Branch 1 not taken.
387 CURL* curl = createCurl(furl, nullptr, false, share);
252
1/2
✓ Branch 0 taken 387 times.
✗ Branch 1 not taken.
387 limitResponseSize(curl);
253
254
3/4
✓ Branch 0 taken 387 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✓ Branch 3 taken 345 times.
387 if (method == "POST") {
255
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 curl_easy_setopt(curl, CURLOPT_POST, 1);
256
2/4
✓ Branch 0 taken 345 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 345 times.
345 } else if (method == "PUT") {
257 ✗ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
258
2/4
✓ Branch 0 taken 345 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 345 times.
345 } else if (method == "PATCH") {
259 ✗ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
260
2/4
✓ Branch 0 taken 345 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 345 times.
345 } else if (method == "DELETE") {
261 ✗ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
262 }
263
264
7/10
✓ Branch 0 taken 387 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 345 times.
✓ Branch 3 taken 42 times.
✓ Branch 4 taken 345 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 345 times.
✓ Branch 8 taken 42 times.
✓ Branch 9 taken 345 times.
387 if ((method == "POST") || (method == "PUT")) {
265
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, data.c_str());
266 }
267
268 387 struct curl_slist* headers = NULL;
269
2/2
✓ Branch 0 taken 78 times.
✓ Branch 1 taken 387 times.
465 for (auto& h : extraHeaders) {
270
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 headers = curl_slist_append(headers, h.c_str());
271 }
272
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 347 times.
387 if (headers) {
273
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
274 }
275
276 // The header list is owned by the callback rather than freed inside it, so
277 // it is released whether the request completes or is cancelled (which
278 // destroys the callback without running it). Either way that happens after
279 // curl_easy_cleanup(), which is also the right order - the old code freed
280 // the list while the easy handle still referenced it.
281 374 std::shared_ptr<curl_slist> headerList(headers, [](curl_slist* h) {
282
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 334 times.
374 if (h) {
283 40 curl_slist_free_all(h);
284 }
285
1/2
✓ Branch 0 taken 387 times.
✗ Branch 1 not taken.
387 });
286
287
3/8
✓ Branch 0 taken 387 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 387 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 387 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
387 addCURL(furl, curl, [callback, headerList](CURL* c) {
288 368 CurlPrivateData* data = nullptr;
289
1/2
✓ Branch 0 taken 368 times.
✗ Branch 1 not taken.
368 curl_easy_getinfo(c, CURLINFO_PRIVATE, &data);
290
1/2
✓ Branch 0 taken 368 times.
✗ Branch 1 not taken.
368 std::string failure = failedTransfer(t_completedResult, data);
291
2/2
✓ Branch 0 taken 47 times.
✓ Branch 1 taken 321 times.
368 if (!failure.empty()) {
292
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 callback(0, failure);
293 47 return;
294 }
295 321 long rc = 0;
296
1/2
✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
321 curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &rc);
297
298 321 char* urlp = nullptr;
299
1/2
✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
321 curl_easy_getinfo(c, CURLINFO_EFFECTIVE_URL, &urlp);
300
1/2
✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
321 std::string resp(reinterpret_cast<char*>(data->resp.data()), data->resp.size());
301
1/2
✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
321 callback(rc, resp);
302
2/2
✓ Branch 0 taken 321 times.
✓ Branch 1 taken 47 times.
368 }, true, owner);
303 387 }
304 320 void CurlManager::addGet(const std::string& url, std::function<void(int rc, const std::string& resp)>&& callback,
305 const std::string& owner, const std::string& cookieSession) {
306
5/10
✓ Branch 0 taken 320 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 320 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 320 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 320 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 320 times.
✗ Branch 9 not taken.
1908 add(url, "GET", "", {}, [callback](int rc, const std::string& resp) { callback(rc, resp); }, owner, cookieSession);
307 320 }
308
309 1 void CurlManager::addPost(const std::string& url, const std::string& data, const std::string& contentType,
310 std::function<void(int rc, const std::string& resp)>&& callback, const std::string& owner) {
311
8/16
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 1 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
7 add(url, "POST", data, { "Content-Type: " + contentType }, [callback](int rc, const std::string& resp) { callback(rc, resp); }, owner);
312
1/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
2 }
313 ✗ void CurlManager::addPut(const std::string& url, const std::string& data, const std::string& contentType,
314 std::function<void(int rc, const std::string& resp)>&& callback, const std::string& owner) {
315 ✗ add(url, "PUT", data, { "Content-Type: " + contentType }, [callback](int rc, const std::string& resp) { callback(rc, resp); }, owner);
316 ✗ }
317
318 863 int CurlManager::cancelRequests(const std::string& owner) {
319
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 863 times.
863 if (owner.empty()) {
320 ✗ return 0; // "" is FPP's own traffic; cancelling all of it is never wanted
321 }
322 // The callbacks are moved out and destroyed after the lock is dropped: they
323 // belong to a plugin, and a captured object's destructor that touched
324 // CurlManager again would deadlock on this non-recursive mutex.
325 863 std::vector<std::function<void(CURL*)>> doomed;
326 {
327
1/2
✓ Branch 0 taken 863 times.
✗ Branch 1 not taken.
863 std::unique_lock<std::mutex> l(lock);
328
2/2
✓ Branch 0 taken 204 times.
✓ Branch 1 taken 863 times.
1067 for (size_t x = 0; x < curls.size(); x++) {
329 204 CurlInfo* ci = curls[x];
330
6/6
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 170 times.
✓ Branch 2 taken 28 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 198 times.
✓ Branch 5 taken 6 times.
204 if (!ci || ci->owner != owner) {
331 198 continue;
332 }
333 6 curls[x] = nullptr;
334 6 --numCurls;
335
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 curl_multi_remove_handle(curlMulti, ci->curl);
336
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (ci->cleanCurl) {
337 6 CurlPrivateData* data = nullptr;
338
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 curl_easy_getinfo(ci->curl, CURLINFO_PRIVATE, &data);
339
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (data) {
340
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 delete data;
341 }
342
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 curl_easy_cleanup(ci->curl);
343 }
344
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 doomed.push_back(std::move(ci->callback));
345
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 delete ci;
346 }
347 863 }
348
1/2
✓ Branch 0 taken 863 times.
✗ Branch 1 not taken.
863 LogDebug(VB_GENERAL, "CurlManager cancelled %d request(s) for %s\n", (int)doomed.size(), owner.c_str());
349 863 return (int)doomed.size();
350 863 }
351
352 // Runs one synchronous request to completion. The main loop pumps curl
353 // too, and when it completes this request it drops numCurls before calling
354 // the callback: processCurls() here then returned false with `done` still
355 // unset, this frame returned, and the callback wrote `done` into it. The
356 // flag is shared instead, and the wait is for this request only (doPut
357 // waited for every request in flight).
358 // Returns the transfer's result.
359 601 static CURLcode runToCompletion(CurlManager& mgr, const std::string& furl, CURL* curl) {
360 struct State {
361 std::atomic<bool> done{ false };
362 CURLcode result = CURLE_OK; // written before done, read after
363 };
364
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 auto state = std::make_shared<State>();
365
3/6
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 601 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 601 times.
✗ Branch 5 not taken.
1202 mgr.addCURL(
366 1803 furl, curl, [state](CURL* c) { state->result = t_completedResult; state->done = true; }, false);
367
2/2
✓ Branch 0 taken 35915 times.
✓ Branch 1 taken 601 times.
36516 while (!state->done) {
368
3/4
✓ Branch 0 taken 35915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 557 times.
✓ Branch 3 taken 35358 times.
35915 if (!mgr.processCurls()) {
369 // another thread holds it between completion and the callback
370
1/2
✓ Branch 0 taken 557 times.
✗ Branch 1 not taken.
557 std::this_thread::sleep_for(std::chrono::milliseconds(1));
371 } else {
372 35358 std::this_thread::yield();
373 }
374 }
375 1202 return state->result;
376 601 }
377
378 601 std::string CurlManager::doGet(const std::string& furl, int& rc, const std::string& cookieSession) {
379
1/4
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
601 CURLSH* share = cookieSession.empty() ? nullptr : getCookieShare(cookieSession);
380
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 CURL* curl = createCurl(furl, nullptr, false, share);
381
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 limitResponseSize(curl);
382
383
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 CURLcode result = runToCompletion(*this, furl, curl);
384
385 601 CurlPrivateData* data = nullptr;
386 601 long rc2 = 0;
387
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 curl_easy_getinfo(curl, CURLINFO_PRIVATE, &data);
388
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc2);
389 // On a failed transfer, rc 0 and the reason, as for one that never connected.
390
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 std::string resp = failedTransfer(result, data);
391
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 rc = resp.empty() ? rc2 : 0;
392
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 if (resp.empty()) {
393
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 resp.assign(reinterpret_cast<char*>(data->resp.data()), data->resp.size());
394 }
395
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 delete data;
396
1/2
✓ Branch 0 taken 601 times.
✗ Branch 1 not taken.
601 curl_easy_cleanup(curl);
397
398 601 return resp;
399 ✗ }
400
401 struct ReadDataInfo {
402 const std::vector<uint8_t>* data;
403 size_t curPos = 0;
404 };
405 ✗ static size_t read_callback(void* ptr, size_t size, size_t nmemb, void* userp) {
406 ✗ size_t buffer_size = size * nmemb;
407 ✗ struct ReadDataInfo* dt = (struct ReadDataInfo*)userp;
408 ✗ int numb = dt->data->size() - dt->curPos;
409 ✗ if (numb > buffer_size) {
410 ✗ numb = buffer_size;
411 }
412 ✗ memcpy(ptr, &(*dt->data)[dt->curPos], numb);
413 ✗ dt->curPos += numb;
414 ✗ return numb;
415 }
416 ✗ std::string CurlManager::doPost(const std::string& furl, const std::string& contentType, const std::vector<uint8_t>& data, int& rc) {
417 ✗ CURL* curl = createCurl(furl);
418 ✗ limitResponseSize(curl);
419
420 ✗ struct curl_slist* head = nullptr;
421 ✗ std::string ct = "Content-Type: " + contentType;
422 ✗ head = curl_slist_append(head, ct.c_str());
423 ✗ std::string cl = "Content-Length: " + std::to_string(data.size());
424 ✗ head = curl_slist_append(head, cl.c_str());
425
426 ✗ curl_easy_setopt(curl, CURLOPT_POST, 1L);
427 ✗ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, head);
428
429 ✗ ReadDataInfo dta;
430 ✗ dta.data = &data;
431 ✗ dta.curPos = 0;
432
433 ✗ curl_easy_setopt(curl, CURLOPT_READDATA, &dta);
434 ✗ curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
435 ✗ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)data.size());
436
437 ✗ CURLcode result = runToCompletion(*this, furl, curl);
438
439 ✗ CurlPrivateData* cdata = nullptr;
440 ✗ long rc2 = 0;
441 ✗ curl_easy_getinfo(curl, CURLINFO_PRIVATE, &cdata);
442 ✗ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc2);
443 // On a failed transfer, rc 0 and the reason, as for one that never connected.
444 ✗ std::string resp = failedTransfer(result, cdata);
445 ✗ rc = resp.empty() ? rc2 : 0;
446 ✗ if (resp.empty()) {
447 ✗ resp.assign(reinterpret_cast<char*>(cdata->resp.data()), cdata->resp.size());
448 }
449 ✗ delete cdata;
450 ✗ curl_slist_free_all(head);
451 ✗ curl_easy_cleanup(curl);
452 ✗ return resp;
453 ✗ }
454 ✗ std::string CurlManager::doPut(const std::string& furl, const std::string& contentType, const std::vector<uint8_t>& data, int& rc) {
455 ✗ CURL* curl = createCurl(furl);
456 ✗ limitResponseSize(curl);
457
458 ✗ struct curl_slist* head = nullptr;
459 ✗ std::string ct = "Content-Type: " + contentType;
460 ✗ head = curl_slist_append(head, ct.c_str());
461 ✗ std::string cl = "Content-Length: " + std::to_string(data.size());
462 ✗ head = curl_slist_append(head, cl.c_str());
463
464 ✗ curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
465 //curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
466 ✗ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, head);
467
468 ✗ ReadDataInfo dta;
469 ✗ dta.data = &data;
470 ✗ dta.curPos = 0;
471
472 ✗ curl_easy_setopt(curl, CURLOPT_READDATA, &dta);
473 ✗ curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
474 ✗ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)data.size());
475
476 ✗ CURLcode result = runToCompletion(*this, furl, curl);
477
478 ✗ CurlPrivateData* cdata = nullptr;
479 ✗ long rc2 = 0;
480 ✗ curl_easy_getinfo(curl, CURLINFO_PRIVATE, &cdata);
481 ✗ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc2);
482 // On a failed transfer, rc 0 and the reason, as for one that never connected.
483 ✗ std::string resp = failedTransfer(result, cdata);
484 ✗ rc = resp.empty() ? rc2 : 0;
485 ✗ if (resp.empty()) {
486 ✗ resp.assign(reinterpret_cast<char*>(cdata->resp.data()), cdata->resp.size());
487 }
488 ✗ delete cdata;
489 ✗ curl_slist_free_all(head);
490 ✗ curl_easy_cleanup(curl);
491 ✗ return resp;
492 ✗ }
493
494 45307 bool CurlManager::doProcessCurls() {
495 CURLMcode mc;
496 45307 int stillRunning = 0;
497
1/2
✓ Branch 0 taken 45307 times.
✗ Branch 1 not taken.
45307 std::unique_lock<std::mutex> l(lock);
498
1/2
✓ Branch 0 taken 45307 times.
✗ Branch 1 not taken.
45307 mc = curl_multi_perform(curlMulti, &stillRunning);
499
2/2
✓ Branch 0 taken 853 times.
✓ Branch 1 taken 44454 times.
45307 if (stillRunning != numCurls) {
500 853 CURLMsg* m = nullptr;
501 do {
502 2113 int msgq = 0;
503
1/2
✓ Branch 0 taken 2113 times.
✗ Branch 1 not taken.
2113 m = curl_multi_info_read(curlMulti, &msgq);
504
3/4
✓ Branch 0 taken 1260 times.
✓ Branch 1 taken 853 times.
✓ Branch 2 taken 1260 times.
✗ Branch 3 not taken.
2113 if (m && (m->msg == CURLMSG_DONE)) {
505 1260 CURL* e = m->easy_handle;
506 // m is invalid once the handle is removed
507 1260 CURLcode result = m->data.result;
508
1/2
✓ Branch 0 taken 1260 times.
✗ Branch 1 not taken.
1260 curl_multi_remove_handle(curlMulti, e);
509 1260 CurlInfo* ci = nullptr;
510
1/2
✓ Branch 0 taken 6456 times.
✗ Branch 1 not taken.
6456 for (int x = 0; x < curls.size(); x++) {
511
6/6
✓ Branch 0 taken 5348 times.
✓ Branch 1 taken 1108 times.
✓ Branch 2 taken 1260 times.
✓ Branch 3 taken 4088 times.
✓ Branch 4 taken 1260 times.
✓ Branch 5 taken 5196 times.
6456 if (curls[x] && curls[x]->curl == e) {
512 1260 ci = curls[x];
513 1260 curls[x] = nullptr;
514 1260 --numCurls;
515 1260 break;
516 }
517 }
518
1/2
✓ Branch 0 taken 1260 times.
✗ Branch 1 not taken.
1260 if (ci) {
519
1/2
✓ Branch 0 taken 1260 times.
✗ Branch 1 not taken.
1260 l.unlock();
520 1260 t_completedResult = result;
521 // The callbacks parse what a remote or a probed device sent
522 // back, on the main loop with nothing above to catch: one
523 // jsoncpp throw on a field of the wrong type ({"Platform":{}}
524 // from a MultiSync remote, {"majorVersion":"9"} from a
525 // discovery probe) ended fppd. Contain it here, for every
526 // callback, and carry on with the cleanup.
527 try {
528
1/2
✓ Branch 0 taken 1260 times.
✗ Branch 1 not taken.
1260 ci->callback(e);
529 ✗ } catch (const std::exception& ex) {
530 ✗ char* url = nullptr;
531 ✗ curl_easy_getinfo(e, CURLINFO_EFFECTIVE_URL, &url);
532 ✗ LogWarn(VB_GENERAL, "Ignoring the reply from %s: handling it failed: %s\n",
533 url ? url : "(unknown)", ex.what());
534 ✗ } catch (...) {
535 ✗ char* url = nullptr;
536 ✗ curl_easy_getinfo(e, CURLINFO_EFFECTIVE_URL, &url);
537 ✗ LogWarn(VB_GENERAL, "Ignoring the reply from %s: handling it failed\n", url ? url : "(unknown)");
538 ✗ }
539 1260 t_completedResult = CURLE_OK;
540
1/2
✓ Branch 0 taken 1260 times.
✗ Branch 1 not taken.
1260 l.lock();
541
2/2
✓ Branch 0 taken 659 times.
✓ Branch 1 taken 601 times.
1260 if (ci->cleanCurl) {
542 659 CurlPrivateData* data = nullptr;
543
1/2
✓ Branch 0 taken 659 times.
✗ Branch 1 not taken.
659 curl_easy_getinfo(e, CURLINFO_PRIVATE, &data);
544
1/2
✓ Branch 0 taken 659 times.
✗ Branch 1 not taken.
659 if (data) {
545
1/2
✓ Branch 0 taken 659 times.
✗ Branch 1 not taken.
659 delete data;
546 }
547
1/2
✓ Branch 0 taken 659 times.
✗ Branch 1 not taken.
659 curl_easy_cleanup(e);
548 }
549
1/2
✓ Branch 0 taken 1260 times.
✗ Branch 1 not taken.
1260 delete ci;
550 }
551 }
552
2/2
✓ Branch 0 taken 1260 times.
✓ Branch 1 taken 853 times.
2113 } while (m);
553 }
554 90614 return numCurls > 0;
555 45307 }
556
557 1342 CurlManager::HostData* CurlManager::getHostData(const std::string& host) {
558 1342 HostData* h = hostData[host];
559
2/2
✓ Branch 0 taken 754 times.
✓ Branch 1 taken 588 times.
1342 if (h == nullptr) {
560
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 754 times.
754 h = new HostData();
561 754 hostData[host] = h;
562 }
563 1342 return h;
564 }
565
566 ✗ void CurlManager::setHostUsernamePassword(const std::string& host, const std::string& username, const std::string password) {
567 ✗ std::unique_lock<std::mutex> l(lock);
568 ✗ HostData* h = getHostData(host);
569 ✗ h->username = username;
570 ✗ h->password = password;
571 ✗ }
572
573 2684 std::string CurlManager::getHost(const std::string& url) {
574 2684 int idx = url.find("://");
575 2684 std::string host = url.substr(idx + 3);
576 2684 idx = host.find("/");
577
1/2
✓ Branch 0 taken 2684 times.
✗ Branch 1 not taken.
2684 host = host.substr(0, idx);
578 2684 return host;
579 ✗ }
580