1 /* Copyright (C) 2018 Wildfire Games.
2 * This file is part of 0 A.D.
3 *
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "precompiled.h"
19
20 #include "UserReport.h"
21
22 #include "lib/timer.h"
23 #include "lib/utf8.h"
24 #include "lib/external_libraries/curl.h"
25 #include "lib/external_libraries/libsdl.h"
26 #include "lib/external_libraries/zlib.h"
27 #include "lib/file/archive/stream.h"
28 #include "lib/os_path.h"
29 #include "lib/sysdep/sysdep.h"
30 #include "ps/ConfigDB.h"
31 #include "ps/Filesystem.h"
32 #include "ps/Profiler2.h"
33 #include "ps/ThreadUtil.h"
34
35 #include <fstream>
36 #include <string>
37
38 #define DEBUG_UPLOADS 0
39
40 /*
41 * The basic idea is that the game submits reports to us, which we send over
42 * HTTP to a server for storage and analysis.
43 *
44 * We can't use libcurl's asynchronous 'multi' API, because DNS resolution can
45 * be synchronous and slow (which would make the game pause).
46 * So we use the 'easy' API in a background thread.
47 * The main thread submits reports, toggles whether uploading is enabled,
48 * and polls for the current status (typically to display in the GUI);
49 * the worker thread does all of the uploading.
50 *
51 * It'd be nice to extend this in the future to handle things like crash reports.
52 * The game should store the crashlogs (suitably anonymised) in a directory, and
53 * we should detect those files and upload them when we're restarted and online.
54 */
55
56
57 /**
58 * Version number stored in config file when the user agrees to the reporting.
59 * Reporting will be disabled if the config value is missing or is less than
60 * this value. If we start reporting a lot more data, we should increase this
61 * value and get the user to re-confirm.
62 */
63 static const int REPORTER_VERSION = 1;
64
65 /**
66 * Time interval (seconds) at which the worker thread will check its reconnection
67 * timers. (This should be relatively high so the thread doesn't waste much time
68 * continually waking up.)
69 */
70 static const double TIMER_CHECK_INTERVAL = 10.0;
71
72 /**
73 * Seconds we should wait before reconnecting to the server after a failure.
74 */
75 static const double RECONNECT_INVERVAL = 60.0;
76
77 CUserReporter g_UserReporter;
78
79 struct CUserReport
80 {
81 time_t m_Time;
82 std::string m_Type;
83 int m_Version;
84 std::string m_Data;
85 };
86
87 class CUserReporterWorker
88 {
89 public:
CUserReporterWorker(const std::string & userID,const std::string & url)90 CUserReporterWorker(const std::string& userID, const std::string& url) :
91 m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"),
92 m_PauseUntilTime(timer_Time()), m_LastUpdateTime(timer_Time())
93 {
94 // Set up libcurl:
95
96 m_Curl = curl_easy_init();
97 ENSURE(m_Curl);
98
99 #if DEBUG_UPLOADS
100 curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L);
101 #endif
102
103 // Capture error messages
104 curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer);
105
106 // Disable signal handlers (required for multithreaded applications)
107 curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L);
108
109 // To minimise security risks, don't support redirects
110 curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L);
111
112 // Prevent this thread from blocking the engine shutdown for 5 minutes in case the server is unavailable
113 curl_easy_setopt(m_Curl, CURLOPT_CONNECTTIMEOUT, 10L);
114
115 // Set IO callbacks
116 curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback);
117 curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this);
118 curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback);
119 curl_easy_setopt(m_Curl, CURLOPT_READDATA, this);
120
121 // Set URL to POST to
122 curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str());
123 curl_easy_setopt(m_Curl, CURLOPT_POST, 1L);
124
125 // Set up HTTP headers
126 m_Headers = NULL;
127 // Set the UA string
128 std::string ua = "User-Agent: 0ad ";
129 ua += curl_version();
130 ua += " (http://play0ad.com/)";
131 m_Headers = curl_slist_append(m_Headers, ua.c_str());
132 // Override the default application/x-www-form-urlencoded type since we're not using that type
133 m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream");
134 // Disable the Accept header because it's a waste of a dozen bytes
135 m_Headers = curl_slist_append(m_Headers, "Accept: ");
136 curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers);
137
138
139 // Set up the worker thread:
140
141 // Use SDL semaphores since OS X doesn't implement sem_init
142 m_WorkerSem = SDL_CreateSemaphore(0);
143 ENSURE(m_WorkerSem);
144
145 int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
146 ENSURE(ret == 0);
147 }
148
~CUserReporterWorker()149 ~CUserReporterWorker()
150 {
151 // Clean up resources
152
153 SDL_DestroySemaphore(m_WorkerSem);
154
155 curl_slist_free_all(m_Headers);
156 curl_easy_cleanup(m_Curl);
157 }
158
159 /**
160 * Called by main thread, when the online reporting is enabled/disabled.
161 */
SetEnabled(bool enabled)162 void SetEnabled(bool enabled)
163 {
164 CScopeLock lock(m_WorkerMutex);
165 if (enabled != m_Enabled)
166 {
167 m_Enabled = enabled;
168
169 // Wake up the worker thread
170 SDL_SemPost(m_WorkerSem);
171 }
172 }
173
174 /**
175 * Called by main thread to request shutdown.
176 * Returns true if we've shut down successfully.
177 * Returns false if shutdown is taking too long (we might be blocked on a
178 * sync network operation) - you mustn't destroy this object, just leak it
179 * and terminate.
180 */
Shutdown()181 bool Shutdown()
182 {
183 {
184 CScopeLock lock(m_WorkerMutex);
185 m_Shutdown = true;
186 }
187
188 // Wake up the worker thread
189 SDL_SemPost(m_WorkerSem);
190
191 // Wait for it to shut down cleanly
192 // TODO: should have a timeout in case of network hangs
193 pthread_join(m_WorkerThread, NULL);
194
195 return true;
196 }
197
198 /**
199 * Called by main thread to determine the current status of the uploader.
200 */
GetStatus()201 std::string GetStatus()
202 {
203 CScopeLock lock(m_WorkerMutex);
204 return m_Status;
205 }
206
207 /**
208 * Called by main thread to add a new report to the queue.
209 */
Submit(const shared_ptr<CUserReport> & report)210 void Submit(const shared_ptr<CUserReport>& report)
211 {
212 {
213 CScopeLock lock(m_WorkerMutex);
214 m_ReportQueue.push_back(report);
215 }
216
217 // Wake up the worker thread
218 SDL_SemPost(m_WorkerSem);
219 }
220
221 /**
222 * Called by the main thread every frame, so we can check
223 * retransmission timers.
224 */
Update()225 void Update()
226 {
227 double now = timer_Time();
228 if (now > m_LastUpdateTime + TIMER_CHECK_INTERVAL)
229 {
230 // Wake up the worker thread
231 SDL_SemPost(m_WorkerSem);
232
233 m_LastUpdateTime = now;
234 }
235 }
236
237 private:
RunThread(void * data)238 static void* RunThread(void* data)
239 {
240 debug_SetThreadName("CUserReportWorker");
241 g_Profiler2.RegisterCurrentThread("userreport");
242
243 static_cast<CUserReporterWorker*>(data)->Run();
244
245 return NULL;
246 }
247
Run()248 void Run()
249 {
250 // Set libcurl's proxy configuration
251 // (This has to be done in the thread because it's potentially very slow)
252 SetStatus("proxy");
253 std::wstring proxy;
254
255 {
256 PROFILE2("get proxy config");
257 if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK)
258 curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str());
259 }
260
261 SetStatus("waiting");
262
263 /*
264 * We use a semaphore to let the thread be woken up when it has
265 * work to do. Various actions from the main thread can wake it:
266 * * SetEnabled()
267 * * Shutdown()
268 * * Submit()
269 * * Retransmission timeouts, once every several seconds
270 *
271 * If multiple actions have triggered wakeups, we might respond to
272 * all of those actions after the first wakeup, which is okay (we'll do
273 * nothing during the subsequent wakeups). We should never hang due to
274 * processing fewer actions than wakeups.
275 *
276 * Retransmission timeouts are triggered via the main thread - we can't simply
277 * use SDL_SemWaitTimeout because on Linux it's implemented as an inefficient
278 * busy-wait loop, and we can't use a manual busy-wait with a long delay time
279 * because we'd lose responsiveness. So the main thread pings the worker
280 * occasionally so it can check its timer.
281 */
282
283 // Wait until the main thread wakes us up
284 while (true)
285 {
286 g_Profiler2.RecordRegionEnter("semaphore wait");
287
288 ENSURE(SDL_SemWait(m_WorkerSem) == 0);
289
290 g_Profiler2.RecordRegionLeave();
291
292 // Handle shutdown requests as soon as possible
293 if (GetShutdown())
294 return;
295
296 // If we're not enabled, ignore this wakeup
297 if (!GetEnabled())
298 continue;
299
300 // If we're still pausing due to a failed connection,
301 // go back to sleep again
302 if (timer_Time() < m_PauseUntilTime)
303 continue;
304
305 // We're enabled, so process as many reports as possible
306 while (ProcessReport())
307 {
308 // Handle shutdowns while we were sending the report
309 if (GetShutdown())
310 return;
311 }
312 }
313 }
314
GetEnabled()315 bool GetEnabled()
316 {
317 CScopeLock lock(m_WorkerMutex);
318 return m_Enabled;
319 }
320
GetShutdown()321 bool GetShutdown()
322 {
323 CScopeLock lock(m_WorkerMutex);
324 return m_Shutdown;
325 }
326
SetStatus(const std::string & status)327 void SetStatus(const std::string& status)
328 {
329 CScopeLock lock(m_WorkerMutex);
330 m_Status = status;
331 #if DEBUG_UPLOADS
332 debug_printf(">>> CUserReporterWorker status: %s\n", status.c_str());
333 #endif
334 }
335
ProcessReport()336 bool ProcessReport()
337 {
338 PROFILE2("process report");
339
340 shared_ptr<CUserReport> report;
341
342 {
343 CScopeLock lock(m_WorkerMutex);
344 if (m_ReportQueue.empty())
345 return false;
346 report = m_ReportQueue.front();
347 m_ReportQueue.pop_front();
348 }
349
350 ConstructRequestData(*report);
351 m_RequestDataOffset = 0;
352 m_ResponseData.clear();
353 m_ErrorBuffer[0] = '\0';
354
355 curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size());
356
357 SetStatus("connecting");
358
359 #if DEBUG_UPLOADS
360 TIMER(L"CUserReporterWorker request");
361 #endif
362
363 CURLcode err = curl_easy_perform(m_Curl);
364
365 #if DEBUG_UPLOADS
366 printf(">>>\n%s\n<<<\n", m_ResponseData.c_str());
367 #endif
368
369 if (err == CURLE_OK)
370 {
371 long code = -1;
372 curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
373 SetStatus("completed:" + CStr::FromInt(code));
374
375 // Check for success code
376 if (code == 200)
377 return true;
378
379 // If the server returns the 410 Gone status, interpret that as meaning
380 // it no longer supports uploads (at least from this version of the game),
381 // so shut down and stop talking to it (to avoid wasting bandwidth)
382 if (code == 410)
383 {
384 CScopeLock lock(m_WorkerMutex);
385 m_Shutdown = true;
386 return false;
387 }
388 }
389 else
390 {
391 std::string errorString(m_ErrorBuffer);
392
393 if (errorString.empty())
394 errorString = curl_easy_strerror(err);
395
396 SetStatus("failed:" + CStr::FromInt(err) + ":" + errorString);
397 }
398
399 // We got an unhandled return code or a connection failure;
400 // push this report back onto the queue and try again after
401 // a long interval
402
403 {
404 CScopeLock lock(m_WorkerMutex);
405 m_ReportQueue.push_front(report);
406 }
407
408 m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL;
409 return false;
410 }
411
ConstructRequestData(const CUserReport & report)412 void ConstructRequestData(const CUserReport& report)
413 {
414 // Construct the POST request data in the application/x-www-form-urlencoded format
415
416 std::string r;
417
418 r += "user_id=";
419 AppendEscaped(r, m_UserID);
420
421 r += "&time=" + CStr::FromInt64(report.m_Time);
422
423 r += "&type=";
424 AppendEscaped(r, report.m_Type);
425
426 r += "&version=" + CStr::FromInt(report.m_Version);
427
428 r += "&data=";
429 AppendEscaped(r, report.m_Data);
430
431 // Compress the content with zlib to save bandwidth.
432 // (Note that we send a request with unlabelled compressed data instead
433 // of using Content-Encoding, because Content-Encoding is a mess and causes
434 // problems with servers and breaks Content-Length and this is much easier.)
435 std::string compressed;
436 compressed.resize(compressBound(r.size()));
437 uLongf destLen = compressed.size();
438 int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size());
439 ENSURE(ok == Z_OK);
440 compressed.resize(destLen);
441
442 m_RequestData.swap(compressed);
443 }
444
AppendEscaped(std::string & buffer,const std::string & str)445 void AppendEscaped(std::string& buffer, const std::string& str)
446 {
447 char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size());
448 buffer += escaped;
449 curl_free(escaped);
450 }
451
ReceiveCallback(void * buffer,size_t size,size_t nmemb,void * userp)452 static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
453 {
454 CUserReporterWorker* self = static_cast<CUserReporterWorker*>(userp);
455
456 if (self->GetShutdown())
457 return 0; // signals an error
458
459 self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
460
461 return size*nmemb;
462 }
463
SendCallback(char * bufptr,size_t size,size_t nmemb,void * userp)464 static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp)
465 {
466 CUserReporterWorker* self = static_cast<CUserReporterWorker*>(userp);
467
468 if (self->GetShutdown())
469 return CURL_READFUNC_ABORT; // signals an error
470
471 // We can return as much data as available, up to the buffer size
472 size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb);
473
474 // ...But restrict to sending a small amount at once, so that we remain
475 // responsive to shutdown requests even if the network is pretty slow
476 amount = std::min((size_t)1024, amount);
477
478 if(amount != 0) // (avoids invalid operator[] call where index=size)
479 {
480 memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount);
481 self->m_RequestDataOffset += amount;
482 }
483
484 self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size()));
485
486 return amount;
487 }
488
489 private:
490 // Thread-related members:
491 pthread_t m_WorkerThread;
492 CMutex m_WorkerMutex;
493 SDL_sem* m_WorkerSem;
494
495 // Shared by main thread and worker thread:
496 // These variables are all protected by m_WorkerMutex
497 std::deque<shared_ptr<CUserReport> > m_ReportQueue;
498 bool m_Enabled;
499 bool m_Shutdown;
500 std::string m_Status;
501
502 // Initialised in constructor by main thread; otherwise used only by worker thread:
503 std::string m_URL;
504 std::string m_UserID;
505 CURL* m_Curl;
506 curl_slist* m_Headers;
507 double m_PauseUntilTime;
508
509 // Only used by worker thread:
510 std::string m_ResponseData;
511 std::string m_RequestData;
512 size_t m_RequestDataOffset;
513 char m_ErrorBuffer[CURL_ERROR_SIZE];
514
515 // Only used by main thread:
516 double m_LastUpdateTime;
517 };
518
519
520
CUserReporter()521 CUserReporter::CUserReporter() :
522 m_Worker(NULL)
523 {
524 }
525
~CUserReporter()526 CUserReporter::~CUserReporter()
527 {
528 ENSURE(!m_Worker); // Deinitialize should have been called before shutdown
529 }
530
LoadUserID()531 std::string CUserReporter::LoadUserID()
532 {
533 std::string userID;
534
535 // Read the user ID from user.cfg (if there is one)
536 CFG_GET_VAL("userreport.id", userID);
537
538 // If we don't have a validly-formatted user ID, generate a new one
539 if (userID.length() != 16)
540 {
541 u8 bytes[8] = {0};
542 sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes));
543 // ignore failures - there's not much we can do about it
544
545 userID = "";
546 for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i)
547 {
548 char hex[3];
549 sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]);
550 userID += hex;
551 }
552
553 g_ConfigDB.SetValueString(CFG_USER, "userreport.id", userID);
554 g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.id", userID);
555 }
556
557 return userID;
558 }
559
IsReportingEnabled()560 bool CUserReporter::IsReportingEnabled()
561 {
562 int version = -1;
563 CFG_GET_VAL("userreport.enabledversion", version);
564 return (version >= REPORTER_VERSION);
565 }
566
SetReportingEnabled(bool enabled)567 void CUserReporter::SetReportingEnabled(bool enabled)
568 {
569 CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0);
570 g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val);
571 g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val);
572
573 if (m_Worker)
574 m_Worker->SetEnabled(enabled);
575 }
576
GetStatus()577 std::string CUserReporter::GetStatus()
578 {
579 if (!m_Worker)
580 return "disabled";
581
582 return m_Worker->GetStatus();
583 }
584
Initialize()585 void CUserReporter::Initialize()
586 {
587 ENSURE(!m_Worker); // must only be called once
588
589 std::string userID = LoadUserID();
590 std::string url;
591 CFG_GET_VAL("userreport.url_upload", url);
592
593 m_Worker = new CUserReporterWorker(userID, url);
594
595 m_Worker->SetEnabled(IsReportingEnabled());
596 }
597
Deinitialize()598 void CUserReporter::Deinitialize()
599 {
600 if (!m_Worker)
601 return;
602
603 if (m_Worker->Shutdown())
604 {
605 // Worker was shut down cleanly
606 SAFE_DELETE(m_Worker);
607 }
608 else
609 {
610 // Worker failed to shut down in a reasonable time
611 // Leak the resources (since that's better than hanging or crashing)
612 m_Worker = NULL;
613 }
614 }
615
Update()616 void CUserReporter::Update()
617 {
618 if (m_Worker)
619 m_Worker->Update();
620 }
621
SubmitReport(const std::string & type,int version,const std::string & data,const std::string & dataHumanReadable)622 void CUserReporter::SubmitReport(const std::string& type, int version, const std::string& data, const std::string& dataHumanReadable)
623 {
624 // Write to logfile, enabling users to assess privacy concerns before the data is submitted
625 if (!dataHumanReadable.empty())
626 {
627 OsPath path = psLogDir() / OsPath("userreport_" + type + ".txt");
628 std::ofstream stream(OsString(path), std::ofstream::trunc);
629 if (stream)
630 {
631 debug_printf("UserReport written to %s\n", path.string8().c_str());
632 stream << dataHumanReadable << std::endl;
633 stream.close();
634 }
635 else
636 debug_printf("Failed to write UserReport to %s\n", path.string8().c_str());
637 }
638
639 // If not initialised, discard the report
640 if (!m_Worker)
641 return;
642
643 // Actual submit
644 shared_ptr<CUserReport> report(new CUserReport);
645 report->m_Time = time(NULL);
646 report->m_Type = type;
647 report->m_Version = version;
648 report->m_Data = data;
649
650 m_Worker->Submit(report);
651 }
652