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