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