1 /*****************************************************************************
2  * PokerTH - The open source texas holdem engine                             *
3  * Copyright (C) 2006-2012 Felix Hammer, Florian Thauer, Lothar May          *
4  *                                                                           *
5  * This program is free software: you can redistribute it and/or modify      *
6  * it under the terms of the GNU Affero General Public License as            *
7  * published by the Free Software Foundation, either version 3 of the        *
8  * License, or (at your option) any later version.                           *
9  *                                                                           *
10  * This program is distributed in the hope that it will be useful,           *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
13  * GNU Affero General Public License for more details.                       *
14  *                                                                           *
15  * You should have received a copy of the GNU Affero General Public License  *
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.     *
17  *                                                                           *
18  *                                                                           *
19  * Additional permission under GNU AGPL version 3 section 7                  *
20  *                                                                           *
21  * If you modify this program, or any covered work, by linking or            *
22  * combining it with the OpenSSL project's OpenSSL library (or a             *
23  * modified version of that library), containing parts covered by the        *
24  * terms of the OpenSSL or SSLeay licenses, the authors of PokerTH           *
25  * (Felix Hammer, Florian Thauer, Lothar May) grant you additional           *
26  * permission to convey the resulting work.                                  *
27  * Corresponding Source for a non-source form of such a combination          *
28  * shall include the source code for the parts of OpenSSL used as well       *
29  * as that of the covered work.                                              *
30  *****************************************************************************/
31 
32 #include "avatarmanager.h"
33 #include <net/net_helper.h>
34 #include <net/socket_msg.h>
35 #include <net/uploaderthread.h>
36 #include <core/loghelper.h>
37 #include <core/crypthelper.h>
38 
39 #include <boost/filesystem.hpp>
40 #include <boost/lambda/lambda.hpp>
41 #include <boost/algorithm/string/predicate.hpp>
42 #include <core/openssl_wrapper.h>
43 
44 #include <fstream>
45 #include <cstring>
46 
47 #define MAX_NUMBER_OF_FILES			NetHelper::GetMaxNumberOfAvatarFiles()
48 #define MAX_AVATAR_CACHE_AGE		NetHelper::GetMaxAvatarCacheAgeSec()
49 
50 #define PNG_HEADER "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
51 #define PNG_HEADER_SIZE (sizeof(PNG_HEADER) - 1)
52 #define JPG_HEADER "\xff\xd8"
53 #define JPG_HEADER_SIZE (sizeof(JPG_HEADER) - 1)
54 #define GIF_HEADER_1 "GIF87a"
55 #define GIF_HEADER_2 "GIF89a"
56 #define GIF_HEADER_SIZE (sizeof(GIF_HEADER_1) - 1)
57 #define MAX_HEADER_SIZE PNG_HEADER_SIZE
58 
59 
60 using namespace std;
61 using namespace boost::filesystem;
62 
63 struct AvatarFileState {
64 	std::ifstream		inputStream;
65 };
66 
AvatarManager(bool useExternalServer,const std::string & externalServerAddress,const string & externalServerUser,const string & externalServerPassword)67 AvatarManager::AvatarManager(bool useExternalServer, const std::string &externalServerAddress,
68 							 const string &externalServerUser, const string &externalServerPassword)
69 	: m_useExternalServer(useExternalServer), m_externalServerAddress(externalServerAddress),
70 	  m_externalServerUser(externalServerUser), m_externalServerPassword(externalServerPassword)
71 {
72 	m_uploader.reset(new UploaderThread());
73 }
74 
~AvatarManager()75 AvatarManager::~AvatarManager()
76 {
77 	m_uploader->SignalTermination();
78 	m_uploader->Join(UPLOADER_THREAD_TERMINATE_TIMEOUT);
79 }
80 
81 bool
Init(const string & dataDir,const string & cacheDir)82 AvatarManager::Init(const string &dataDir, const string &cacheDir)
83 {
84 	bool retVal = true;
85 	bool tmpRet;
86 	path tmpCachePath(cacheDir);
87 	path tmpDataPath(dataDir);
88 	{
89 		boost::mutex::scoped_lock lock(m_cacheDirMutex);
90 		m_cacheDir = tmpCachePath.directory_string();
91 	}
92 	{
93 		boost::mutex::scoped_lock lock(m_avatarsMutex);
94 		tmpRet = InternalReadDirectory((tmpDataPath / "gfx/avatars/default/people/").directory_string(), m_avatars);
95 		retVal = retVal && tmpRet;
96 		tmpRet = InternalReadDirectory((tmpDataPath / "gfx/avatars/default/misc/").directory_string(), m_avatars);
97 		retVal = retVal && tmpRet;
98 	}
99 	if (cacheDir.empty() || tmpCachePath.empty())
100 		LOG_ERROR("Cache directory was not set!");
101 	else {
102 		boost::mutex::scoped_lock lock(m_cachedAvatarsMutex);
103 		tmpRet = InternalReadDirectory(tmpCachePath.directory_string(), m_cachedAvatars);
104 		retVal = retVal && tmpRet;
105 	}
106 
107 	m_uploader->Run();
108 	return retVal;
109 }
110 
111 bool
AddSingleAvatar(const std::string & fileName)112 AvatarManager::AddSingleAvatar(const std::string &fileName)
113 {
114 	bool retVal = false;
115 	path filePath(fileName);
116 	string tmpFileName(filePath.file_string());
117 
118 	if (!fileName.empty() && !tmpFileName.empty()) {
119 		unsigned outFileSize = 0;
120 		AvatarFileType outFileType;
121 		boost::shared_ptr<AvatarFileState> tmpFileState = OpenAvatarFileForChunkRead(tmpFileName, outFileSize, outFileType);
122 
123 		// Check whether the avatar file is valid.
124 		if (tmpFileState.get()) {
125 			tmpFileState.reset();
126 
127 			MD5Buf md5buf;
128 			if (CryptHelper::MD5Sum(tmpFileName, md5buf)) {
129 				boost::mutex::scoped_lock lock(m_avatarsMutex);
130 				m_avatars.insert(AvatarMap::value_type(md5buf, tmpFileName));
131 				retVal = true;
132 			}
133 		}
134 	}
135 	return retVal;
136 }
137 
138 boost::shared_ptr<AvatarFileState>
OpenAvatarFileForChunkRead(const std::string & fileName,unsigned & outFileSize,AvatarFileType & outFileType)139 AvatarManager::OpenAvatarFileForChunkRead(const std::string &fileName, unsigned &outFileSize, AvatarFileType &outFileType)
140 {
141 	outFileSize = 0;
142 	boost::shared_ptr<AvatarFileState> retVal;
143 	try {
144 		outFileType = GetAvatarFileType(fileName);
145 		boost::shared_ptr<AvatarFileState> fileState(new AvatarFileState);
146 		fileState->inputStream.open(fileName.c_str(), ios_base::in | ios_base::binary);
147 		if (!fileState->inputStream.fail()) {
148 			// Find out file size.
149 			// Not fully portable, but works on win/linux/mac.
150 			fileState->inputStream.seekg(0, ios_base::beg);
151 			std::streampos startPos = fileState->inputStream.tellg();
152 			fileState->inputStream.seekg(0, ios_base::end);
153 			std::streampos endPos = fileState->inputStream.tellg();
154 			fileState->inputStream.seekg(0, ios_base::beg);
155 			std::streamoff posDiff(endPos - startPos);
156 			outFileSize = (unsigned)posDiff;
157 			if (outFileSize >= MIN_AVATAR_FILE_SIZE && outFileSize <= MAX_AVATAR_FILE_SIZE) {
158 				// Validate type of file by verifying image header.
159 				unsigned char fileHeader[MAX_HEADER_SIZE];
160 				fileState->inputStream.read((char *)fileHeader, sizeof(fileHeader));
161 				fileState->inputStream.seekg(0, ios_base::beg);
162 
163 				if (IsValidAvatarFileType(outFileType, fileHeader, sizeof(fileHeader)))
164 					retVal = fileState;
165 			}
166 		}
167 	} catch (...) {
168 		LOG_ERROR("Exception caught when trying to open avatar.");
169 	}
170 	return retVal;
171 }
172 
173 unsigned
ChunkReadAvatarFile(boost::shared_ptr<AvatarFileState> fileState,unsigned char * data,unsigned chunkSize)174 AvatarManager::ChunkReadAvatarFile(boost::shared_ptr<AvatarFileState> fileState, unsigned char *data, unsigned chunkSize)
175 {
176 	unsigned retVal = 0;
177 	if (fileState.get()) {
178 		try {
179 			if (!fileState->inputStream.fail() && !fileState->inputStream.eof()) {
180 				fileState->inputStream.read((char *)data, chunkSize);
181 				retVal = static_cast<unsigned>(fileState->inputStream.gcount());
182 			}
183 		} catch (...) {
184 			LOG_ERROR("Exception caught when trying to read avatar.");
185 		}
186 	}
187 	return retVal;
188 }
189 
190 int
AvatarFileToNetPackets(const string & fileName,unsigned requestId,NetPacketList & packets)191 AvatarManager::AvatarFileToNetPackets(const string &fileName, unsigned requestId, NetPacketList &packets)
192 {
193 	int retVal = ERR_NET_INVALID_AVATAR_FILE;
194 	unsigned fileSize = 0;
195 	AvatarFileType fileType;
196 	boost::shared_ptr<AvatarFileState> tmpState = OpenAvatarFileForChunkRead(fileName, fileSize, fileType);
197 	if (tmpState.get() && fileSize && fileType != AVATAR_FILE_TYPE_UNKNOWN) {
198 		boost::shared_ptr<NetPacket> avatarHeader(new NetPacket);
199 		avatarHeader->GetMsg()->set_messagetype(PokerTHMessage::Type_AvatarHeaderMessage);
200 		AvatarHeaderMessage *netHeader = avatarHeader->GetMsg()->mutable_avatarheadermessage();
201 		netHeader->set_requestid(requestId);
202 		netHeader->set_avatartype(static_cast<NetAvatarType>(fileType));
203 		netHeader->set_avatarsize(fileSize);
204 		packets.push_back(avatarHeader);
205 
206 		unsigned numBytes = 0;
207 		unsigned totalBytesRead = 0;
208 		vector<unsigned char> tmpData(MAX_FILE_DATA_SIZE);
209 		do {
210 			numBytes = ChunkReadAvatarFile(tmpState, &tmpData[0], MAX_FILE_DATA_SIZE);
211 			if (numBytes) {
212 				totalBytesRead += numBytes;
213 
214 				boost::shared_ptr<NetPacket> avatarFile(new NetPacket);
215 				avatarFile->GetMsg()->set_messagetype(PokerTHMessage::Type_AvatarDataMessage);
216 				AvatarDataMessage *netFile = avatarFile->GetMsg()->mutable_avatardatamessage();
217 				netFile->set_requestid(requestId);
218 				netFile->set_avatarblock((const char *)&tmpData[0], numBytes);
219 				packets.push_back(avatarFile);
220 			}
221 		} while (numBytes);
222 
223 		if (fileSize != totalBytesRead)
224 			retVal = ERR_NET_WRONG_AVATAR_SIZE;
225 		else {
226 			boost::shared_ptr<NetPacket> avatarEnd(new NetPacket);
227 			avatarEnd->GetMsg()->set_messagetype(PokerTHMessage::Type_AvatarEndMessage);
228 			AvatarEndMessage *netEnd = avatarEnd->GetMsg()->mutable_avatarendmessage();
229 			netEnd->set_requestid(requestId);
230 			packets.push_back(avatarEnd);
231 			retVal = 0;
232 		}
233 	}
234 	return retVal;
235 }
236 
237 AvatarFileType
GetAvatarFileType(const string & fileName)238 AvatarManager::GetAvatarFileType(const string &fileName)
239 {
240 	AvatarFileType fileType;
241 
242 	path filePath(fileName);
243 	string ext(extension(filePath));
244 	if (boost::algorithm::iequals(ext, ".png"))
245 		fileType = AVATAR_FILE_TYPE_PNG;
246 	else if (boost::algorithm::iequals(ext, ".jpg") || boost::algorithm::iequals(ext, ".jpeg"))
247 		fileType = AVATAR_FILE_TYPE_JPG;
248 	else if (boost::algorithm::iequals(ext, ".gif"))
249 		fileType = AVATAR_FILE_TYPE_GIF;
250 	else
251 		fileType = AVATAR_FILE_TYPE_UNKNOWN;
252 
253 	return fileType;
254 }
255 
256 string
GetAvatarFileExtension(AvatarFileType fileType)257 AvatarManager::GetAvatarFileExtension(AvatarFileType fileType)
258 {
259 	string ext;
260 	switch (fileType) {
261 	case AVATAR_FILE_TYPE_PNG:
262 		ext = ".png";
263 		break;
264 	case AVATAR_FILE_TYPE_JPG:
265 		ext = ".jpg";
266 		break;
267 	case AVATAR_FILE_TYPE_GIF:
268 		ext = ".gif";
269 		break;
270 	case AVATAR_FILE_TYPE_UNKNOWN:
271 		break;
272 	}
273 	return ext;
274 }
275 
276 bool
GetHashForAvatar(const std::string & fileName,MD5Buf & md5buf) const277 AvatarManager::GetHashForAvatar(const std::string &fileName, MD5Buf &md5buf) const
278 {
279 	bool found = false;
280 	if (exists(fileName)) {
281 		// Scan default avatars first.
282 		{
283 			boost::mutex::scoped_lock lock(m_avatarsMutex);
284 			AvatarMap::const_iterator i = m_avatars.begin();
285 			AvatarMap::const_iterator end = m_avatars.end();
286 			while (i != end) {
287 				if (i->second == fileName) {
288 					md5buf = i->first;
289 					found = true;
290 					break;
291 				}
292 				++i;
293 			}
294 		}
295 		// Check cached avatars next.
296 		if (!found) {
297 			boost::mutex::scoped_lock lock(m_cachedAvatarsMutex);
298 			AvatarMap::const_iterator i = m_cachedAvatars.begin();
299 			AvatarMap::const_iterator end = m_cachedAvatars.end();
300 			while (i != end) {
301 				if (i->second == fileName) {
302 					md5buf = i->first;
303 					found = true;
304 					break;
305 				}
306 				++i;
307 			}
308 		}
309 
310 		// Calculate md5 sum if not found.
311 		if (!found) {
312 			if (CryptHelper::MD5Sum(fileName, md5buf))
313 				found = true;
314 		}
315 	}
316 	return found;
317 }
318 
319 bool
GetAvatarFileName(const MD5Buf & md5buf,std::string & fileName) const320 AvatarManager::GetAvatarFileName(const MD5Buf &md5buf, std::string &fileName) const
321 {
322 	bool retVal = false;
323 	{
324 		boost::mutex::scoped_lock lock(m_avatarsMutex);
325 		AvatarMap::const_iterator pos = m_avatars.find(md5buf);
326 		if (pos != m_avatars.end()) {
327 			fileName = pos->second;
328 			retVal = true;
329 		}
330 	}
331 	if (!retVal) {
332 		boost::mutex::scoped_lock lock(m_cachedAvatarsMutex);
333 		AvatarMap::const_iterator pos = m_cachedAvatars.find(md5buf);
334 		if (pos != m_cachedAvatars.end()) {
335 			fileName = pos->second;
336 			retVal = true;
337 		}
338 	}
339 	return retVal;
340 }
341 
342 bool
HasAvatar(const MD5Buf & md5buf) const343 AvatarManager::HasAvatar(const MD5Buf &md5buf) const
344 {
345 	string tmpFile;
346 	return GetAvatarFileName(md5buf, tmpFile);
347 }
348 
349 bool
StoreAvatarInCache(const MD5Buf & md5buf,AvatarFileType avatarFileType,const unsigned char * data,size_t size,bool upload)350 AvatarManager::StoreAvatarInCache(const MD5Buf &md5buf, AvatarFileType avatarFileType, const unsigned char *data, size_t size, bool upload)
351 {
352 	bool retVal = false;
353 	string cacheDir;
354 	{
355 		boost::mutex::scoped_lock lock(m_cacheDirMutex);
356 		cacheDir = m_cacheDir;
357 	}
358 	try {
359 		string ext(GetAvatarFileExtension(avatarFileType));
360 		if (!ext.empty() && !cacheDir.empty()) {
361 			// Check header before storing file.
362 			if (IsValidAvatarFileType(avatarFileType, data, size)) {
363 				path tmpPath(cacheDir);
364 				tmpPath /= (md5buf.ToString() + ext);
365 				string fileName(tmpPath.file_string());
366 				std::ofstream o(fileName.c_str(), ios_base::out | ios_base::binary | ios_base::trunc);
367 				if (!o.fail()) {
368 					o.write((const char *)data, size);
369 					o.close();
370 					if (upload && m_useExternalServer) {
371 						m_uploader->QueueUpload(m_externalServerAddress, m_externalServerUser, m_externalServerPassword, fileName, size);
372 					}
373 
374 					{
375 						boost::mutex::scoped_lock lock(m_cachedAvatarsMutex);
376 						m_cachedAvatars.insert(AvatarMap::value_type(md5buf, fileName));
377 					}
378 					retVal = true;
379 				}
380 			}
381 		}
382 	} catch (...) {
383 		LOG_ERROR("Exception caught when trying to store avatar.");
384 	}
385 	return retVal;
386 }
387 
388 bool
IsValidAvatarFileType(AvatarFileType avatarFileType,const unsigned char * fileHeader,size_t fileHeaderSize)389 AvatarManager::IsValidAvatarFileType(AvatarFileType avatarFileType, const unsigned char *fileHeader, size_t fileHeaderSize)
390 {
391 	bool validType = false;
392 
393 	switch (avatarFileType) {
394 	case AVATAR_FILE_TYPE_PNG:
395 		if (fileHeaderSize >= PNG_HEADER_SIZE
396 				&& memcmp(fileHeader, PNG_HEADER, PNG_HEADER_SIZE) == 0) {
397 			validType = true;
398 		}
399 		break;
400 	case AVATAR_FILE_TYPE_JPG:
401 		if (fileHeaderSize >= JPG_HEADER_SIZE
402 				&& memcmp(fileHeader, JPG_HEADER, JPG_HEADER_SIZE) == 0) {
403 			validType = true;
404 		}
405 		break;
406 	case AVATAR_FILE_TYPE_GIF:
407 		if (fileHeaderSize >= GIF_HEADER_SIZE
408 				&& (memcmp(fileHeader, GIF_HEADER_1, GIF_HEADER_SIZE) == 0
409 					|| memcmp(fileHeader, GIF_HEADER_2, GIF_HEADER_SIZE) == 0)) {
410 			validType = true;
411 		}
412 		break;
413 	case AVATAR_FILE_TYPE_UNKNOWN:
414 		break;
415 	}
416 	return validType;
417 }
418 
419 void
RemoveOldAvatarCacheEntries()420 AvatarManager::RemoveOldAvatarCacheEntries()
421 {
422 	string cacheDir;
423 	{
424 		boost::mutex::scoped_lock lock(m_cacheDirMutex);
425 		cacheDir = m_cacheDir;
426 	}
427 	try {
428 		path cachePath(cacheDir);
429 		cacheDir = cachePath.directory_string();
430 		// Never delete anything if we do not have a special cache dir set.
431 		if (!cacheDir.empty()) {
432 			boost::mutex::scoped_lock lock(m_cachedAvatarsMutex);
433 
434 			// First pass: Remove files which no longer exist.
435 			// Count files and record age.
436 			AvatarList removeList;
437 			TimeAvatarMap timeMap;
438 			{
439 				AvatarMap::const_iterator i = m_cachedAvatars.begin();
440 				AvatarMap::const_iterator end = m_cachedAvatars.end();
441 				while (i != end) {
442 					bool keepFile = false;
443 					path filePath(i->second);
444 					string fileString(filePath.file_string());
445 					// Only consider files which are definitely in the cache dir.
446 					if (fileString.size() > cacheDir.size() && fileString.substr(0, cacheDir.size()) == cacheDir) {
447 						// Only consider files with MD5 as file name.
448 						MD5Buf tmpBuf;
449 						if (exists(filePath) && tmpBuf.FromString(basename(filePath))) {
450 							timeMap.insert(TimeAvatarMap::value_type(last_write_time(filePath), i->first));
451 							keepFile = true;
452 						}
453 					}
454 					if (!keepFile)
455 						removeList.push_back(i->first);
456 
457 					++i;
458 				}
459 			}
460 
461 			{
462 				AvatarList::const_iterator i = removeList.begin();
463 				AvatarList::const_iterator end = removeList.end();
464 				while (i != end) {
465 					m_cachedAvatars.erase(*i);
466 					++i;
467 				}
468 				removeList.clear();
469 			}
470 
471 			// Remove and physically delete files in one of the
472 			// following cases:
473 			// 1. More than MAX_NUMBER_OF_FILES files are present
474 			//    - delete until only MAX_NUMBER_OF_FILES/2 are left.
475 			// 2. Files are older than 30 days.
476 
477 			if (m_cachedAvatars.size() > MAX_NUMBER_OF_FILES) {
478 				while (!timeMap.empty() && m_cachedAvatars.size() > MAX_NUMBER_OF_FILES / 2) {
479 					TimeAvatarMap::iterator i = timeMap.begin();
480 					AvatarMap::iterator pos = m_cachedAvatars.find(i->second);
481 					if (pos != m_cachedAvatars.end()) {
482 						path tmpPath(pos->second);
483 						remove(tmpPath);
484 						m_cachedAvatars.erase(pos);
485 					}
486 					timeMap.erase(i);
487 				}
488 			}
489 
490 			// Get reference time.
491 			time_t curTime = time(NULL);
492 			while (!timeMap.empty() && !m_cachedAvatars.empty()) {
493 				TimeAvatarMap::iterator i = timeMap.begin();
494 				if (curTime - i->first < (int)MAX_AVATAR_CACHE_AGE)
495 					break;
496 				AvatarMap::iterator pos = m_cachedAvatars.find(i->second);
497 				if (pos != m_cachedAvatars.end()) {
498 					path tmpPath(pos->second);
499 					remove(tmpPath);
500 					m_cachedAvatars.erase(pos);
501 				}
502 				timeMap.erase(i);
503 			}
504 		}
505 	} catch (...) {
506 		LOG_ERROR("Exception caught while cleaning up cache.");
507 	}
508 }
509 
510 bool
InternalReadDirectory(const std::string & dir,AvatarMap & avatars)511 AvatarManager::InternalReadDirectory(const std::string &dir, AvatarMap &avatars)
512 {
513 	bool retVal = true;
514 	path tmpPath(dir);
515 
516 	if (exists(tmpPath) && is_directory(tmpPath)) {
517 		try {
518 			// This method is not thread safe. Only call after locking the map.
519 			directory_iterator i(tmpPath);
520 			directory_iterator end;
521 
522 			while (i != end) {
523 				if (is_regular(i->status())) {
524 					string md5sum(basename(i->path()));
525 					MD5Buf md5buf;
526 					string fileName(i->path().file_string());
527 					if (md5buf.FromString(md5sum)) {
528 						// Only consider files with md5sum as name.
529 						avatars.insert(AvatarMap::value_type(md5buf, fileName));
530 					}
531 				}
532 				++i;
533 			}
534 		} catch (...) {
535 			LOG_ERROR("Exception caught when trying to scan avatar directory.");
536 			retVal = false;
537 		}
538 	} else {
539 		LOG_ERROR("Avatar directory does not exist.");
540 		retVal = false;
541 	}
542 	return retVal;
543 }
544 
545