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