GCC Code Coverage Report


Directory: ./
File: CurlManager.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 13 14 92.9%
Functions: 8 8 100.0%
Branches: 3 6 50.0%

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 // PLUGIN ABI: native plugins compile against this header and are loaded with
14 // dlopen(), so parts of it are baked into binaries FPP did not build.
15 // - CurlManager::CurlPrivateData: plugins read req/resp from the object
16 // FPP hands back, so its members and their order are fixed.
17 // - Signatures of addGet()/addPost()/addCURL()/createCurl(): add new
18 // parameters as a new overload and keep the old one.
19 // Any such change must bump FPP_PLUGIN_API_VERSION in Plugin.h. Without the
20 // bump, a plugin built against the previous header still loads and silently
21 // uses the old layout - FPP 10.2 shipped exactly that and crash-looped fppd.
22 // See .claude/PLUGIN-ABI.md for which plugins use what.
23
24 #pragma once
25
26 #include <curl/curl.h>
27 #include <atomic>
28 #include <functional>
29 #include <list>
30 #include <map>
31 #include <mutex>
32 #include <vector>
33
34 class CurlManager {
35 public:
36 static CurlManager INSTANCE;
37
38 class CurlPrivateData {
39 public:
40 1342 CurlPrivateData() {}
41 5064 virtual ~CurlPrivateData() {
42
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1266 times.
2532 if (req) {
43 ✗ delete req;
44 }
45 5064 }
46
47 std::string host;
48 std::vector<uint8_t> resp;
49 char errorResp[CURL_ERROR_SIZE] = { 0 };
50
51 std::vector<uint8_t>* req = nullptr;
52 size_t curPos = 0;
53 };
54
55 // The trailing `owner` on every add below tags the request so it can be
56 // cancelled as a group later. A plugin that wants to be unloadable must pass
57 // its plugin name: its callback is a std::function whose code and captured
58 // state live in the plugin's .so, so an in-flight request is a call into
59 // that library after it is gone. FPP's own callers leave it empty - core
60 // code is never unloaded.
61
62 // A non-empty `cookieSession` on the calls below joins the request to a
63 // named cookie jar shared by every request using the same token: the handle
64 // is given CURLOPT_SHARE on that session's CURLSH plus an empty
65 // CURLOPT_COOKIEFILE to arm the cookie engine, so a PHPSESSID (or any other
66 // Set-Cookie) established by one request is presented by the next. The
67 // ordering the caller wants between requests is the caller's job (chain the
68 // next add from the previous one's callback); the session only carries the
69 // cookies. The CURLSH is created on first use and must outlive every handle
70 // that references it - see releaseCookieSession().
71
72 // Raw methods for working directly with the CURL* objects. `share`, when
73 // non-null, is a CURLSH from getCookieShare()/a cookie session.
74 CURL* createCurl(const std::string& fullUrl, CurlPrivateData** data = nullptr, bool upload = false, CURLSH* share = nullptr);
75 // Caps the body a handle from createCurl() will hold (16MB): past it the
76 // transfer ends with CURLE_WRITE_ERROR, although the HTTP status still
77 // reads as received. FPP's own add*()/do*() requests have it and report
78 // that as a failure (rc 0); a handle from createCurl() has no limit unless
79 // this is called on it.
80 static void limitResponseSize(CURL* c);
81 void addCURL(const std::string& furl, CURL* curl, std::function<void(CURL*)>&& callback, bool autoCleanCurl = true,
82 const std::string& owner = "");
83
84 // Asynchronous methods for string content
85 void add(const std::string& furl, const std::string& method, const std::string& data,
86 const std::list<std::string>& extraHeaders,
87 std::function<void(int rc, const std::string& resp)>&& callback,
88 const std::string& owner = "", const std::string& cookieSession = "");
89 void addGet(const std::string& furl, std::function<void(int rc, const std::string& resp)>&& callback,
90 const std::string& owner = "", const std::string& cookieSession = "");
91 void addPost(const std::string& furl, const std::string& data, const std::string& contentType,
92 std::function<void(int rc, const std::string& resp)>&& callback, const std::string& owner = "");
93 void addPut(const std::string& furl, const std::string& data, const std::string& contentType,
94 std::function<void(int rc, const std::string& resp)>&& callback, const std::string& owner = "");
95
96 // Abandons every outstanding request tagged with `owner`: the transfers are
97 // removed from the multi handle and their callbacks are destroyed WITHOUT
98 // being invoked. Destroying them is the point - that is what releases the
99 // plugin-owned callable while its library is still mapped. Returns how many
100 // were cancelled.
101 //
102 // Main loop only, same as doProcessCurls(), so it cannot run while a
103 // callback is executing.
104 int cancelRequests(const std::string& owner);
105
106 // Ends a cookie session created via a `cookieSession` token and frees its
107 // CURLSH. The caller MUST guarantee no handle still references the share:
108 // cancel this session's owner (cancelRequests) or let its requests finish
109 // first. Cleaning up a share still referenced by an easy handle is undefined
110 // in libcurl, so this checks curl's CURLSHE_IN_USE and refuses (leaks + logs)
111 // rather than free an in-use share. A missing/never-created token is a no-op.
112 void releaseCookieSession(const std::string& token);
113
114 // Synchronous methods. A non-empty `cookieSession` shares cookies as above.
115 std::string doGet(const std::string& furl, int& rc, const std::string& cookieSession = "");
116 std::string doPost(const std::string& furl, const std::string& contentType, const std::vector<uint8_t>& data, int& rc);
117 std::string doPut(const std::string& furl, const std::string& contentType, const std::vector<uint8_t>& data, int& rc);
118
119 void setHostUsernamePassword(const std::string& host, const std::string& username, const std::string password);
120 512542 bool processCurls() {
121
2/2
✓ Branch 0 taken 45307 times.
✓ Branch 1 taken 467235 times.
512542 if (numCurls) {
122 45307 return doProcessCurls();
123 }
124 467235 return false;
125 }
126
127 private:
128 CurlManager();
129 ~CurlManager();
130
131 bool doProcessCurls();
132 std::string getHost(const std::string& url);
133
134 // Finds or creates the CURLSH backing a cookie session token. The share has
135 // CURL_LOCK_DATA_COOKIE enabled and LOCK/UNLOCK callbacks wired to the
136 // session's own mutex, so it is safe even if a transfer using it is driven
137 // from a thread other than the one that armed it (processCurls() is called
138 // from a few call sites). Takes `lock`.
139 CURLSH* getCookieShare(const std::string& token);
140
141 CURLM* curlMulti = nullptr;
142 // Read without the lock by the inline processCurls(), from every thread
143 // that pumps curl, while addCURL() and completions change it. Atomic, and
144 // the same size and alignment as the int it replaces, so a plugin built
145 // against the old header still reads it at the same place.
146 std::atomic<int> numCurls{ 0 };
147 static_assert(sizeof(std::atomic<int>) == sizeof(int) && alignof(std::atomic<int>) == alignof(int),
148 "numCurls must keep the layout of an int (PLUGIN ABI)");
149
150 class CurlInfo {
151 public:
152 1342 CurlInfo() {}
153 1266 ~CurlInfo() {}
154 std::string host;
155 std::string url;
156 std::string owner; // empty for FPP's own requests; see cancelRequests()
157 CURL* curl = nullptr;
158 bool cleanCurl = true;
159 std::function<void(CURL*)> callback;
160 };
161 std::vector<CurlInfo*> curls;
162 std::mutex lock;
163
164 class HostData {
165 public:
166 754 HostData() {}
167 ~HostData() {}
168 std::string username;
169 std::string password;
170 int authStatus = 0;
171 };
172 std::map<std::string, HostData*> hostData;
173 HostData* getHostData(const std::string& host);
174
175 // One shared cookie jar per session token. `mutex` backs the share's
176 // LOCK/UNLOCK callbacks; it lives here (heap, stable address) so the pointer
177 // handed to CURLSHOPT_USERDATA stays valid for the share's whole life. The
178 // map itself is guarded by `lock`.
179 class CookieSession {
180 public:
181 5 CookieSession() {}
182 5 ~CookieSession() {}
183 CURLSH* share = nullptr;
184 std::mutex mutex;
185 };
186 std::map<std::string, CookieSession*> cookieSessions;
187 };
188