1 /*
2     Copyright © 2015-2019 by The qTox Project Contributors
3 
4     This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6     qTox is libre software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     qTox is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with qTox.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 
21 #include "corefile.h"
22 #include "core.h"
23 #include "toxfile.h"
24 #include "toxstring.h"
25 #include "src/persistence/settings.h"
26 #include "src/model/status.h"
27 #include "src/model/toxclientstandards.h"
28 #include "src/util/compatiblerecursivemutex.h"
29 #include <QDebug>
30 #include <QDir>
31 #include <QFile>
32 #include <QRegularExpression>
33 #include <QThread>
34 #include <cassert>
35 #include <memory>
36 
37 /**
38  * @class CoreFile
39  * @brief Manages the file transfer service of toxcore
40  */
41 
makeCoreFile(Core * core,Tox * tox,CompatibleRecursiveMutex & coreLoopLock)42 CoreFilePtr CoreFile::makeCoreFile(Core *core, Tox *tox, CompatibleRecursiveMutex &coreLoopLock)
43 {
44     assert(core != nullptr);
45     assert(tox != nullptr);
46     connectCallbacks(*tox);
47     CoreFilePtr result = CoreFilePtr{new CoreFile{tox, coreLoopLock}};
48     connect(core, &Core::friendStatusChanged, result.get(), &CoreFile::onConnectionStatusChanged);
49 
50     return result;
51 }
52 
CoreFile(Tox * core,CompatibleRecursiveMutex & coreLoopLock)53 CoreFile::CoreFile(Tox *core, CompatibleRecursiveMutex &coreLoopLock)
54     : tox{core}
55     , coreLoopLock{&coreLoopLock}
56 {
57 }
58 
59 /**
60  * @brief Get corefile iteration interval.
61  *
62  * tox_iterate calls to get good file transfer performances
63  * @return The maximum amount of time in ms that Core should wait between two tox_iterate() calls.
64  */
corefileIterationInterval()65 unsigned CoreFile::corefileIterationInterval()
66 {
67     /*
68        Sleep at most 1000ms if we have no FT, 10 for user FTs
69        There is no real difference between 10ms sleep and 50ms sleep when it
70        comes to CPU usage – just keep the CPU usage low when there are no file
71        transfers, and speed things up when there is an ongoing file transfer.
72     */
73     constexpr unsigned fileInterval = 10, idleInterval = 1000;
74 
75     for (ToxFile& file : fileMap) {
76         if (file.status == ToxFile::TRANSMITTING) {
77             return fileInterval;
78         }
79     }
80     return idleInterval;
81 }
82 
connectCallbacks(Tox & tox)83 void CoreFile::connectCallbacks(Tox &tox)
84 {
85     // be careful not to to reconnect already used callbacks here
86     tox_callback_file_chunk_request(&tox, CoreFile::onFileDataCallback);
87     tox_callback_file_recv(&tox, CoreFile::onFileReceiveCallback);
88     tox_callback_file_recv_chunk(&tox, CoreFile::onFileRecvChunkCallback);
89     tox_callback_file_recv_control(&tox, CoreFile::onFileControlCallback);
90 }
91 
sendAvatarFile(uint32_t friendId,const QByteArray & data)92 void CoreFile::sendAvatarFile(uint32_t friendId, const QByteArray& data)
93 {
94     QMutexLocker{coreLoopLock};
95 
96     uint64_t filesize = 0;
97     uint8_t *file_id = nullptr;
98     uint8_t *file_name = nullptr;
99     size_t nameLength = 0;
100     uint8_t avatarHash[TOX_HASH_LENGTH];
101     if (!data.isEmpty()) {
102         static_assert(TOX_HASH_LENGTH <= TOX_FILE_ID_LENGTH, "TOX_HASH_LENGTH > TOX_FILE_ID_LENGTH!");
103         tox_hash(avatarHash, (uint8_t*)data.data(), data.size());
104         filesize = data.size();
105         file_id = avatarHash;
106         file_name = avatarHash;
107         nameLength = TOX_HASH_LENGTH;
108     }
109     Tox_Err_File_Send error;
110     const uint32_t fileNum = tox_file_send(tox, friendId, TOX_FILE_KIND_AVATAR, filesize,
111                                     file_id, file_name, nameLength, &error);
112 
113     switch (error) {
114     case TOX_ERR_FILE_SEND_OK:
115         break;
116     case TOX_ERR_FILE_SEND_FRIEND_NOT_CONNECTED:
117         qCritical() << "Friend not connected";
118         return;
119     case TOX_ERR_FILE_SEND_FRIEND_NOT_FOUND:
120         qCritical() << "Friend not found";
121         return;
122     case TOX_ERR_FILE_SEND_NAME_TOO_LONG:
123         qCritical() << "Name too long";
124         return;
125     case TOX_ERR_FILE_SEND_NULL:
126         qCritical() << "Send null";
127         return;
128     case TOX_ERR_FILE_SEND_TOO_MANY:
129         qCritical() << "To many ougoing transfer";
130         return;
131     default:
132         return;
133     }
134 
135     ToxFile file{fileNum, friendId, "", "", ToxFile::SENDING};
136     file.filesize = filesize;
137     file.fileKind = TOX_FILE_KIND_AVATAR;
138     file.avatarData = data;
139     file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
140     tox_file_get_file_id(tox, friendId, fileNum, (uint8_t*)file.resumeFileId.data(),
141                          nullptr);
142     addFile(friendId, fileNum, file);
143 }
144 
sendFile(uint32_t friendId,QString filename,QString filePath,long long filesize)145 void CoreFile::sendFile(uint32_t friendId, QString filename, QString filePath,
146                         long long filesize)
147 {
148     QMutexLocker{coreLoopLock};
149 
150     ToxString fileName(filename);
151     TOX_ERR_FILE_SEND sendErr;
152     uint32_t fileNum = tox_file_send(tox, friendId, TOX_FILE_KIND_DATA, filesize,
153                                      nullptr, fileName.data(), fileName.size(), &sendErr);
154     if (sendErr != TOX_ERR_FILE_SEND_OK) {
155         qWarning() << "sendFile: Can't create the Tox file sender (" << sendErr << ")";
156         emit fileSendFailed(friendId, fileName.getQString());
157         return;
158     }
159     qDebug() << QString("sendFile: Created file sender %1 with friend %2").arg(fileNum).arg(friendId);
160 
161     ToxFile file{fileNum, friendId, fileName.getQString(), filePath, ToxFile::SENDING};
162     file.filesize = filesize;
163     file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
164     tox_file_get_file_id(tox, friendId, fileNum, (uint8_t*)file.resumeFileId.data(),
165                          nullptr);
166     if (!file.open(false)) {
167         qWarning() << QString("sendFile: Can't open file, error: %1").arg(file.file->errorString());
168     }
169 
170     addFile(friendId, fileNum, file);
171 
172     emit fileSendStarted(file);
173 }
174 
pauseResumeFile(uint32_t friendId,uint32_t fileId)175 void CoreFile::pauseResumeFile(uint32_t friendId, uint32_t fileId)
176 {
177     QMutexLocker{coreLoopLock};
178 
179     ToxFile* file = findFile(friendId, fileId);
180     if (!file) {
181         qWarning("pauseResumeFileSend: No such file in queue");
182         return;
183     }
184 
185     if (file->status != ToxFile::TRANSMITTING && file->status != ToxFile::PAUSED) {
186         qWarning() << "pauseResumeFileSend: File is stopped";
187         return;
188     }
189 
190     file->pauseStatus.localPauseToggle();
191 
192     if (file->pauseStatus.paused()) {
193         file->status = ToxFile::PAUSED;
194         emit fileTransferPaused(*file);
195     } else {
196         file->status = ToxFile::TRANSMITTING;
197         emit fileTransferAccepted(*file);
198     }
199 
200     if (file->pauseStatus.localPaused()) {
201         tox_file_control(tox, file->friendId, file->fileNum, TOX_FILE_CONTROL_PAUSE,
202                          nullptr);
203     } else {
204         tox_file_control(tox, file->friendId, file->fileNum, TOX_FILE_CONTROL_RESUME,
205                          nullptr);
206     }
207 }
208 
cancelFileSend(uint32_t friendId,uint32_t fileId)209 void CoreFile::cancelFileSend(uint32_t friendId, uint32_t fileId)
210 {
211     QMutexLocker{coreLoopLock};
212 
213     ToxFile* file = findFile(friendId, fileId);
214     if (!file) {
215         qWarning("cancelFileSend: No such file in queue");
216         return;
217     }
218 
219     file->status = ToxFile::CANCELED;
220     emit fileTransferCancelled(*file);
221     tox_file_control(tox, file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
222     removeFile(friendId, fileId);
223 }
224 
cancelFileRecv(uint32_t friendId,uint32_t fileId)225 void CoreFile::cancelFileRecv(uint32_t friendId, uint32_t fileId)
226 {
227     QMutexLocker{coreLoopLock};
228 
229     ToxFile* file = findFile(friendId, fileId);
230     if (!file) {
231         qWarning("cancelFileRecv: No such file in queue");
232         return;
233     }
234     file->status = ToxFile::CANCELED;
235     emit fileTransferCancelled(*file);
236     tox_file_control(tox, file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
237     removeFile(friendId, fileId);
238 }
239 
rejectFileRecvRequest(uint32_t friendId,uint32_t fileId)240 void CoreFile::rejectFileRecvRequest(uint32_t friendId, uint32_t fileId)
241 {
242     QMutexLocker{coreLoopLock};
243 
244     ToxFile* file = findFile(friendId, fileId);
245     if (!file) {
246         qWarning("rejectFileRecvRequest: No such file in queue");
247         return;
248     }
249     file->status = ToxFile::CANCELED;
250     emit fileTransferCancelled(*file);
251     tox_file_control(tox, file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
252     removeFile(friendId, fileId);
253 }
254 
acceptFileRecvRequest(uint32_t friendId,uint32_t fileId,QString path)255 void CoreFile::acceptFileRecvRequest(uint32_t friendId, uint32_t fileId, QString path)
256 {
257     QMutexLocker{coreLoopLock};
258 
259     ToxFile* file = findFile(friendId, fileId);
260     if (!file) {
261         qWarning("acceptFileRecvRequest: No such file in queue");
262         return;
263     }
264     file->setFilePath(path);
265     if (!file->open(true)) {
266         qWarning() << "acceptFileRecvRequest: Unable to open file";
267         return;
268     }
269     file->status = ToxFile::TRANSMITTING;
270     emit fileTransferAccepted(*file);
271     tox_file_control(tox, file->friendId, file->fileNum, TOX_FILE_CONTROL_RESUME, nullptr);
272 }
273 
findFile(uint32_t friendId,uint32_t fileId)274 ToxFile* CoreFile::findFile(uint32_t friendId, uint32_t fileId)
275 {
276     QMutexLocker{coreLoopLock};
277 
278     uint64_t key = getFriendKey(friendId, fileId);
279     if (fileMap.contains(key)) {
280         return &fileMap[key];
281     }
282 
283     qWarning() << "findFile: File transfer with ID" << friendId << ':' << fileId << "doesn't exist";
284     return nullptr;
285 }
286 
addFile(uint32_t friendId,uint32_t fileId,const ToxFile & file)287 void CoreFile::addFile(uint32_t friendId, uint32_t fileId, const ToxFile& file)
288 {
289     uint64_t key = getFriendKey(friendId, fileId);
290 
291     if (fileMap.contains(key)) {
292         qWarning() << "addFile: Overwriting existing file transfer with same ID" << friendId << ':'
293                    << fileId;
294     }
295 
296     fileMap.insert(key, file);
297 }
298 
removeFile(uint32_t friendId,uint32_t fileId)299 void CoreFile::removeFile(uint32_t friendId, uint32_t fileId)
300 {
301     uint64_t key = getFriendKey(friendId, fileId);
302     if (!fileMap.contains(key)) {
303         qWarning() << "removeFile: No such file in queue";
304         return;
305     }
306     fileMap[key].file->close();
307     fileMap.remove(key);
308 }
309 
getCleanFileName(QString filename)310 QString CoreFile::getCleanFileName(QString filename)
311 {
312     QRegularExpression regex{QStringLiteral(R"([<>:"/\\|?])")};
313     filename.replace(regex, "_");
314 
315     return filename;
316 }
317 
318 void CoreFile::onFileReceiveCallback(Tox* tox, uint32_t friendId, uint32_t fileId, uint32_t kind,
319                                      uint64_t filesize, const uint8_t* fname, size_t fnameLen,
320                                      void* vCore)
321 {
322     Core* core = static_cast<Core*>(vCore);
323     CoreFile* coreFile = core->getCoreFile();
324     auto filename = ToxString(fname, fnameLen);
325     const ToxPk friendPk = core->getFriendPublicKey(friendId);
326 
327     if (kind == TOX_FILE_KIND_AVATAR) {
328         if (!filesize) {
329             qDebug() << QString("Received empty avatar request %1:%2").arg(friendId).arg(fileId);
330             // Avatars of size 0 means explicitely no avatar
331             tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
332             emit core->friendAvatarRemoved(core->getFriendPublicKey(friendId));
333             return;
334         } else {
335             if (!ToxClientStandards::IsValidAvatarSize(filesize)) {
336                 qWarning() <<
337                     QString("Received avatar request from %1 with size %2.").arg(friendId).arg(filesize) +
338                     QString(" The max size allowed for avatars is %3. Cancelling transfer.").arg(ToxClientStandards::MaxAvatarSize);
339                 tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
340                 return;
341             }
342             static_assert(TOX_HASH_LENGTH <= TOX_FILE_ID_LENGTH,
343                           "TOX_HASH_LENGTH > TOX_FILE_ID_LENGTH!");
344             uint8_t avatarHash[TOX_FILE_ID_LENGTH];
345             tox_file_get_file_id(tox, friendId, fileId, avatarHash, nullptr);
346             QByteArray avatarBytes{static_cast<const char*>(static_cast<const void*>(avatarHash)),
347                                    TOX_HASH_LENGTH};
348             emit core->fileAvatarOfferReceived(friendId, fileId, avatarBytes);
349             return;
350         }
351     } else {
352         const auto cleanFileName = CoreFile::getCleanFileName(filename.getQString());
353         if (cleanFileName != filename.getQString()) {
354             qDebug() << QStringLiteral("Cleaned filename");
355             filename = ToxString(cleanFileName);
356             emit coreFile->fileNameChanged(friendPk);
357         } else {
358             qDebug() << QStringLiteral("filename already clean");
359         }
360         qDebug() << QString("Received file request %1:%2 kind %3").arg(friendId).arg(fileId).arg(kind);
361     }
362 
363     ToxFile file{fileId, friendId, filename.getBytes(), "", ToxFile::RECEIVING};
364     file.filesize = filesize;
365     file.fileKind = kind;
366     file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
367     tox_file_get_file_id(tox, friendId, fileId, (uint8_t*)file.resumeFileId.data(),
368                          nullptr);
369     coreFile->addFile(friendId, fileId, file);
370     if (kind != TOX_FILE_KIND_AVATAR) {
371         emit coreFile->fileReceiveRequested(file);
372     }
373 }
374 
375 // TODO(sudden6): This whole method is a mess but needed to get stuff working for now
376 void CoreFile::handleAvatarOffer(uint32_t friendId, uint32_t fileId, bool accept)
377 {
378     if (!accept) {
379         // If it's an avatar but we already have it cached, cancel
380         qDebug() << QString("Received avatar request %1:%2, reject, since we have it in cache.")
381                         .arg(friendId)
382                         .arg(fileId);
383         tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
384         return;
385     }
386 
387     // It's an avatar and we don't have it, autoaccept the transfer
388     qDebug() << QString("Received avatar request %1:%2, accept, since we don't have it "
389                         "in cache.")
390                     .arg(friendId)
391                     .arg(fileId);
392     tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_RESUME, nullptr);
393 
394     ToxFile file{fileId, friendId, "<avatar>", "", ToxFile::RECEIVING};
395     file.filesize = 0;
396     file.fileKind = TOX_FILE_KIND_AVATAR;
397     file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
398     tox_file_get_file_id(tox, friendId, fileId, (uint8_t*)file.resumeFileId.data(),
399                          nullptr);
400     addFile(friendId, fileId, file);
401 }
402 
403 void CoreFile::onFileControlCallback(Tox*, uint32_t friendId, uint32_t fileId,
404                                      Tox_File_Control control, void* vCore)
405 {
406     Core* core = static_cast<Core*>(vCore);
407     CoreFile* coreFile = core->getCoreFile();
408     ToxFile* file = coreFile->findFile(friendId, fileId);
409     if (!file) {
410         qWarning("onFileControlCallback: No such file in queue");
411         return;
412     }
413 
414     if (control == TOX_FILE_CONTROL_CANCEL) {
415         if (file->fileKind != TOX_FILE_KIND_AVATAR)
416             qDebug() << "File tranfer" << friendId << ":" << fileId << "cancelled by friend";
417         file->status = ToxFile::CANCELED;
418         emit coreFile->fileTransferCancelled(*file);
419         coreFile->removeFile(friendId, fileId);
420     } else if (control == TOX_FILE_CONTROL_PAUSE) {
421         qDebug() << "onFileControlCallback: Received pause for file " << friendId << ":" << fileId;
422         file->pauseStatus.remotePause();
423         file->status = ToxFile::PAUSED;
424         emit coreFile->fileTransferRemotePausedUnpaused(*file, true);
425     } else if (control == TOX_FILE_CONTROL_RESUME) {
426         if (file->direction == ToxFile::SENDING && file->fileKind == TOX_FILE_KIND_AVATAR)
427             qDebug() << "Avatar transfer" << fileId << "to friend" << friendId << "accepted";
428         else
429             qDebug() << "onFileControlCallback: Received resume for file " << friendId << ":" << fileId;
430         file->pauseStatus.remoteResume();
431         file->status = file->pauseStatus.paused() ? ToxFile::PAUSED : ToxFile::TRANSMITTING;
432         emit coreFile->fileTransferRemotePausedUnpaused(*file, false);
433     } else {
434         qWarning() << "Unhandled file control " << control << " for file " << friendId << ':' << fileId;
435     }
436 }
437 
438 void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId, uint64_t pos,
439                                   size_t length, void* vCore)
440 {
441 
442     Core* core = static_cast<Core*>(vCore);
443     CoreFile* coreFile = core->getCoreFile();
444     ToxFile* file = coreFile->findFile(friendId, fileId);
445     if (!file) {
446         qWarning("onFileDataCallback: No such file in queue");
447         return;
448     }
449 
450     // If we reached EOF, ack and cleanup the transfer
451     if (!length) {
452         file->status = ToxFile::FINISHED;
453         if (file->fileKind != TOX_FILE_KIND_AVATAR) {
454             emit coreFile->fileTransferFinished(*file);
455             emit coreFile->fileUploadFinished(file->filePath);
456         }
457         coreFile->removeFile(friendId, fileId);
458         return;
459     }
460 
461     std::unique_ptr<uint8_t[]> data(new uint8_t[length]);
462     int64_t nread;
463 
464     if (file->fileKind == TOX_FILE_KIND_AVATAR) {
465         QByteArray chunk = file->avatarData.mid(pos, length);
466         nread = chunk.size();
467         memcpy(data.get(), chunk.data(), nread);
468     } else {
469         file->file->seek(pos);
470         nread = file->file->read((char*)data.get(), length);
471         if (nread <= 0) {
472             qWarning("onFileDataCallback: Failed to read from file");
473             file->status = ToxFile::CANCELED;
474             emit coreFile->fileTransferCancelled(*file);
475             tox_file_send_chunk(tox, friendId, fileId, pos, nullptr, 0, nullptr);
476             coreFile->removeFile(friendId, fileId);
477             return;
478         }
479         file->bytesSent += length;
480         file->hashGenerator->addData((const char*)data.get(), length);
481     }
482 
483     if (!tox_file_send_chunk(tox, friendId, fileId, pos, data.get(), nread, nullptr)) {
484         qWarning("onFileDataCallback: Failed to send data chunk");
485         return;
486     }
487     if (file->fileKind != TOX_FILE_KIND_AVATAR) {
488         emit coreFile->fileTransferInfo(*file);
489     }
490 }
491 
492 void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fileId, uint64_t position,
493                                        const uint8_t* data, size_t length, void* vCore)
494 {
495     Core* core = static_cast<Core*>(vCore);
496     CoreFile* coreFile = core->getCoreFile();
497     ToxFile* file = coreFile->findFile(friendId, fileId);
498     if (!file) {
499         qWarning("onFileRecvChunkCallback: No such file in queue");
500         tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
501         return;
502     }
503 
504     if (file->bytesSent != position) {
505         qWarning("onFileRecvChunkCallback: Received a chunk out-of-order, aborting transfer");
506         if (file->fileKind != TOX_FILE_KIND_AVATAR) {
507             file->status = ToxFile::CANCELED;
508             emit coreFile->fileTransferCancelled(*file);
509         }
510         tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
511         coreFile->removeFile(friendId, fileId);
512         return;
513     }
514 
515     if (!length) {
516         file->status = ToxFile::FINISHED;
517         if (file->fileKind == TOX_FILE_KIND_AVATAR) {
518             QPixmap pic;
519             pic.loadFromData(file->avatarData);
520             if (!pic.isNull()) {
521                 qDebug() << "Got" << file->avatarData.size() << "bytes of avatar data from" << friendId;
522                 emit core->friendAvatarChanged(core->getFriendPublicKey(friendId), file->avatarData);
523             }
524         } else {
525             emit coreFile->fileTransferFinished(*file);
526             emit coreFile->fileDownloadFinished(file->filePath);
527         }
528         coreFile->removeFile(friendId, fileId);
529         return;
530     }
531 
532     if (file->fileKind == TOX_FILE_KIND_AVATAR) {
533         file->avatarData.append((char*)data, length);
534     } else {
535         file->file->write((char*)data, length);
536     }
537     file->bytesSent += length;
538     file->hashGenerator->addData((const char*)data, length);
539 
540     if (file->fileKind != TOX_FILE_KIND_AVATAR) {
541         emit coreFile->fileTransferInfo(*file);
542     }
543 }
544 
545 void CoreFile::onConnectionStatusChanged(uint32_t friendId, Status::Status state)
546 {
547     bool isOffline = state == Status::Status::Offline;
548     // TODO: Actually resume broken file transfers
549     // We need to:
550     // - Start a new file transfer with the same 32byte file ID with toxcore
551     // - Seek to the correct position again
552     // - Update the fileNum in our ToxFile
553     // - Update the users of our signals to check the 32byte tox file ID, not the uint32_t file_num
554     // (fileId)
555     ToxFile::FileStatus status = !isOffline ? ToxFile::TRANSMITTING : ToxFile::BROKEN;
556     for (uint64_t key : fileMap.keys()) {
557         if (key >> 32 != friendId)
558             continue;
559         fileMap[key].status = status;
560         emit fileTransferBrokenUnbroken(fileMap[key], isOffline);
561     }
562 }
563