1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 #include <algorithm>
7 #include <cerrno>
8 #include <cstring>
9 #include <dlfcn.h>
10 #include <string>
11 #include <unistd.h>
12 #include "mozilla/Unused.h"
13 #include "third_party/curl/curl.h"
14
15 namespace PingSender {
16
17 using std::string;
18
19 using mozilla::Unused;
20
21 /**
22 * A simple wrapper around libcurl "easy" functions. Provides RAII opening
23 * and initialization of the curl library
24 */
25 class CurlWrapper {
26 public:
27 CurlWrapper();
28 ~CurlWrapper();
29 bool Init();
30 bool IsValidDestination(const string& url);
31 bool Post(const string& url, const string& payload);
32
33 // libcurl functions
34 CURL* (*easy_init)(void);
35 CURLcode (*easy_setopt)(CURL*, CURLoption, ...);
36 CURLcode (*easy_perform)(CURL*);
37 CURLcode (*easy_getinfo)(CURL*, CURLINFO, ...);
38 curl_slist* (*slist_append)(curl_slist*, const char*);
39 void (*slist_free_all)(curl_slist*);
40 const char* (*easy_strerror)(CURLcode);
41 void (*easy_cleanup)(CURL*);
42 void (*global_cleanup)(void);
43
44 CURLU* (*curl_url)();
45 CURLUcode (*curl_url_get)(CURLU*, CURLUPart, char**, unsigned int);
46 CURLUcode (*curl_url_set)(CURLU*, CURLUPart, const char*, unsigned int);
47 void (*curl_free)(char*);
48 void (*curl_url_cleanup)(CURLU*);
49
50 private:
51 void* mLib;
52 void* mCurl;
53 bool mCanParseUrl;
54 };
55
CurlWrapper()56 CurlWrapper::CurlWrapper()
57 : easy_init(nullptr),
58 easy_setopt(nullptr),
59 easy_perform(nullptr),
60 easy_getinfo(nullptr),
61 slist_append(nullptr),
62 slist_free_all(nullptr),
63 easy_strerror(nullptr),
64 easy_cleanup(nullptr),
65 global_cleanup(nullptr),
66 curl_url(nullptr),
67 curl_url_get(nullptr),
68 curl_url_set(nullptr),
69 curl_free(nullptr),
70 curl_url_cleanup(nullptr),
71 mLib(nullptr),
72 mCurl(nullptr) {}
73
~CurlWrapper()74 CurlWrapper::~CurlWrapper() {
75 if (mLib) {
76 if (mCurl && easy_cleanup) {
77 easy_cleanup(mCurl);
78 }
79
80 if (global_cleanup) {
81 global_cleanup();
82 }
83
84 dlclose(mLib);
85 }
86 }
87
Init()88 bool CurlWrapper::Init() {
89 const char* libcurlPaths[] = {
90 #if defined(XP_MACOSX)
91 // macOS
92 "/usr/lib/libcurl.dylib",
93 "/usr/lib/libcurl.4.dylib",
94 "/usr/lib/libcurl.3.dylib",
95 #else // Linux, *BSD, ...
96 "libcurl.so",
97 "libcurl.so.4",
98 // Debian gives libcurl a different name when it is built against GnuTLS
99 "libcurl-gnutls.so",
100 "libcurl-gnutls.so.4",
101 // Older versions in case we find nothing better
102 "libcurl.so.3",
103 "libcurl-gnutls.so.3", // See above for Debian
104 #endif
105 };
106
107 // libcurl might show up under different names & paths, try them all until
108 // we find it
109 for (const char* libname : libcurlPaths) {
110 mLib = dlopen(libname, RTLD_NOW);
111
112 if (mLib) {
113 break;
114 }
115 }
116
117 if (!mLib) {
118 PINGSENDER_LOG("ERROR: Could not find libcurl\n");
119 return false;
120 }
121
122 *(void**)(&easy_init) = dlsym(mLib, "curl_easy_init");
123 *(void**)(&easy_setopt) = dlsym(mLib, "curl_easy_setopt");
124 *(void**)(&easy_perform) = dlsym(mLib, "curl_easy_perform");
125 *(void**)(&easy_getinfo) = dlsym(mLib, "curl_easy_getinfo");
126 *(void**)(&slist_append) = dlsym(mLib, "curl_slist_append");
127 *(void**)(&slist_free_all) = dlsym(mLib, "curl_slist_free_all");
128 *(void**)(&easy_strerror) = dlsym(mLib, "curl_easy_strerror");
129 *(void**)(&easy_cleanup) = dlsym(mLib, "curl_easy_cleanup");
130 *(void**)(&global_cleanup) = dlsym(mLib, "curl_global_cleanup");
131
132 *(void**)(&curl_url) = dlsym(mLib, "curl_url");
133 *(void**)(&curl_url_set) = dlsym(mLib, "curl_url_set");
134 *(void**)(&curl_url_get) = dlsym(mLib, "curl_url_get");
135 *(void**)(&curl_free) = dlsym(mLib, "curl_free");
136 *(void**)(&curl_url_cleanup) = dlsym(mLib, "curl_url_cleanup");
137
138 if (!easy_init || !easy_setopt || !easy_perform || !easy_getinfo ||
139 !slist_append || !slist_free_all || !easy_strerror || !easy_cleanup ||
140 !global_cleanup) {
141 PINGSENDER_LOG("ERROR: libcurl is missing one of the required symbols\n");
142 return false;
143 }
144
145 mCanParseUrl = true;
146 if (!curl_url || !curl_url_get || !curl_url_set || !curl_free ||
147 !curl_url_cleanup) {
148 mCanParseUrl = false;
149 PINGSENDER_LOG("WARNING: Do not have url parsing functions in libcurl\n");
150 }
151
152 mCurl = easy_init();
153
154 if (!mCurl) {
155 PINGSENDER_LOG("ERROR: Could not initialize libcurl\n");
156 return false;
157 }
158
159 return true;
160 }
161
DummyWriteCallback(char * ptr,size_t size,size_t nmemb,void * userdata)162 static size_t DummyWriteCallback(char* ptr, size_t size, size_t nmemb,
163 void* userdata) {
164 Unused << ptr;
165 Unused << size;
166 Unused << nmemb;
167 Unused << userdata;
168
169 return size * nmemb;
170 }
171
172 // If we can't use curl's URL parsing (which is safer) we have to fallback
173 // to this handwritten one (which is only as safe as we are clever.)
FallbackIsValidDestination(const string & aUrl)174 bool FallbackIsValidDestination(const string& aUrl) {
175 // Lowercase the url
176 string url = aUrl;
177 std::transform(url.begin(), url.end(), url.begin(),
178 [](unsigned char c) { return std::tolower(c); });
179 // Strip off the scheme in the beginning
180 if (url.find("http://") == 0) {
181 url = url.substr(7);
182 } else if (url.find("https://") == 0) {
183 url = url.substr(8);
184 }
185
186 // Remove any user information. If a @ appeared in the userinformation,
187 // it would need to be encoded.
188 unsigned long atStart = url.find_first_of("@");
189 url = (atStart == std::string::npos) ? url : url.substr(atStart + 1);
190
191 // Remove any path or fragment information, leaving us with a url that may
192 // contain host, and port.
193 unsigned long fragStart = url.find_first_of("#");
194 url = (fragStart == std::string::npos) ? url : url.substr(0, fragStart);
195 unsigned long pathStart = url.find_first_of("/");
196 url = (pathStart == std::string::npos) ? url : url.substr(0, pathStart);
197
198 // Remove the port, because we run tests targeting localhost:port
199 unsigned long portStart = url.find_last_of(":");
200 url = (portStart == std::string::npos) ? url : url.substr(0, portStart);
201
202 return ::IsValidDestination(url);
203 }
204
IsValidDestination(const string & aUrl)205 bool CurlWrapper::IsValidDestination(const string& aUrl) {
206 if (!mCanParseUrl) {
207 return FallbackIsValidDestination(aUrl);
208 }
209
210 bool ret = false;
211 CURLU* h = curl_url();
212 if (!h) {
213 return FallbackIsValidDestination(aUrl);
214 }
215
216 if (CURLUE_OK != curl_url_set(h, CURLUPART_URL, aUrl.c_str(), 0)) {
217 goto cleanup;
218 }
219
220 char* host;
221 if (CURLUE_OK != curl_url_get(h, CURLUPART_HOST, &host, 0)) {
222 goto cleanup;
223 }
224
225 ret = ::IsValidDestination(host);
226 curl_free(host);
227
228 cleanup:
229 curl_url_cleanup(h);
230 return ret;
231 }
232
Post(const string & url,const string & payload)233 bool CurlWrapper::Post(const string& url, const string& payload) {
234 easy_setopt(mCurl, CURLOPT_URL, url.c_str());
235 easy_setopt(mCurl, CURLOPT_USERAGENT, kUserAgent);
236 easy_setopt(mCurl, CURLOPT_WRITEFUNCTION, DummyWriteCallback);
237
238 // Build the date header.
239 std::string dateHeader = GenerateDateHeader();
240
241 // Set the custom headers.
242 curl_slist* headerChunk = nullptr;
243 headerChunk = slist_append(headerChunk, kCustomVersionHeader);
244 headerChunk = slist_append(headerChunk, kContentEncodingHeader);
245 headerChunk = slist_append(headerChunk, dateHeader.c_str());
246 CURLcode err = easy_setopt(mCurl, CURLOPT_HTTPHEADER, headerChunk);
247 if (err != CURLE_OK) {
248 PINGSENDER_LOG("ERROR: Failed to set HTTP headers, %s\n",
249 easy_strerror(err));
250 slist_free_all(headerChunk);
251 return false;
252 }
253
254 // Set the size of the POST data
255 easy_setopt(mCurl, CURLOPT_POSTFIELDSIZE, payload.length());
256
257 // Set the contents of the POST data
258 easy_setopt(mCurl, CURLOPT_POSTFIELDS, payload.c_str());
259
260 // Fail if the server returns a 4xx code
261 easy_setopt(mCurl, CURLOPT_FAILONERROR, 1);
262
263 // Override the default connection timeout, which is 5 minutes.
264 easy_setopt(mCurl, CURLOPT_CONNECTTIMEOUT_MS, kConnectionTimeoutMs);
265
266 // Block until the operation is performend. Ignore the response, if the POST
267 // fails we can't do anything about it.
268 err = easy_perform(mCurl);
269 // Whatever happens, we want to clean up the header memory.
270 slist_free_all(headerChunk);
271
272 if (err != CURLE_OK) {
273 PINGSENDER_LOG("ERROR: Failed to send HTTP request, %s\n",
274 easy_strerror(err));
275 return false;
276 }
277
278 return true;
279 }
280
Post(const string & url,const string & payload)281 bool Post(const string& url, const string& payload) {
282 CurlWrapper curl;
283
284 if (!curl.Init()) {
285 return false;
286 }
287 if (!curl.IsValidDestination(url)) {
288 PINGSENDER_LOG("ERROR: Invalid destination host\n");
289 return false;
290 }
291
292 return curl.Post(url, payload);
293 }
294
ChangeCurrentWorkingDirectory(const string & pingPath)295 void ChangeCurrentWorkingDirectory(const string& pingPath) {
296 // This is not needed under Linux/macOS
297 }
298
299 } // namespace PingSender
300