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