/* * Squawk messenger. * Copyright (C) 2019 Yury Gubich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "networkaccess.h" Core::NetworkAccess::NetworkAccess(QObject* parent): QObject(parent), running(false), manager(0), storage("fileURLStorage"), downloads(), uploads(), currentPath() { QSettings settings; currentPath = settings.value("downloadsPath").toString(); } Core::NetworkAccess::~NetworkAccess() { stop(); } void Core::NetworkAccess::downladFile(const QString& url) { std::map::iterator itr = downloads.find(url); if (itr != downloads.end()) { qDebug() << "NetworkAccess received a request to download a file" << url << ", but the file is currently downloading, skipping"; } else { try { std::pair> p = storage.getPath(url); if (p.first.size() > 0) { QFileInfo info(p.first); if (info.exists() && info.isFile()) emit downloadFileComplete(p.second, p.first); else startDownload(p.second, url); } else { startDownload(p.second, url); } } catch (const LMDBAL::NotFound& e) { qDebug() << "NetworkAccess received a request to download a file" << url << ", but there is now record of which message uses that file, downloading anyway"; storage.addFile(url); startDownload(std::list(), url); } catch (const LMDBAL::Unknown& e) { qDebug() << "Error requesting file path:" << e.what(); emit loadFileError(std::list(), QString("Database error: ") + e.what(), false); } } } void Core::NetworkAccess::start() { if (!running) { manager = new QNetworkAccessManager(); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) manager->setTransferTimeout(); #endif storage.open(); running = true; } } void Core::NetworkAccess::stop() { if (running) { storage.close(); manager->deleteLater(); manager = 0; running = false; for (std::map::const_iterator itr = downloads.begin(), end = downloads.end(); itr != end; ++itr) { itr->second->success = false; itr->second->reply->abort(); //assuming it's gonna call onRequestFinished slot } } } void Core::NetworkAccess::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = downloads.find(url); if (itr == downloads.end()) { qDebug() << "an error downloading" << url << ": the request had some progress but seems like no one is waiting for it, skipping"; } else { Transfer* dwn = itr->second; if (dwn->success) { qreal received = bytesReceived; qreal total = bytesTotal; qreal progress = received/total; dwn->progress = progress; emit loadFileProgress(dwn->messages, progress, false); } } } void Core::NetworkAccess::onDownloadError(QNetworkReply::NetworkError code) { qDebug() << "DEBUG: DOWNLOAD ERROR"; QNetworkReply* rpl = static_cast(sender()); qDebug() << rpl->errorString(); QString url = rpl->url().toString(); std::map::const_iterator itr = downloads.find(url); if (itr == downloads.end()) { qDebug() << "an error downloading" << url << ": the request is reporting an error but seems like no one is waiting for it, skipping"; } else { QString errorText = getErrorText(code); //if (errorText.size() > 0) { itr->second->success = false; Transfer* dwn = itr->second; emit loadFileError(dwn->messages, errorText, false); //} } } void Core::NetworkAccess::onDownloadSSLError(const QList& errors) { qDebug() << "DEBUG: DOWNLOAD SSL ERRORS"; for (const QSslError& err : errors) qDebug() << err.errorString(); QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = downloads.find(url); if (itr == downloads.end()) { qDebug() << "an SSL error downloading" << url << ": the request is reporting an error but seems like no one is waiting for it, skipping"; } else { //if (errorText.size() > 0) { itr->second->success = false; Transfer* dwn = itr->second; emit loadFileError(dwn->messages, "SSL errors occured", false); //} } } QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) { QString errorText(""); switch (code) { case QNetworkReply::NoError: //this never is supposed to happen break; // network layer errors [relating to the destination server] (1-99): case QNetworkReply::ConnectionRefusedError: errorText = "Connection refused"; break; case QNetworkReply::RemoteHostClosedError: errorText = "Remote server closed the connection"; break; case QNetworkReply::HostNotFoundError: errorText = "Remote host is not found"; break; case QNetworkReply::TimeoutError: errorText = "Connection was closed because it timed out"; break; case QNetworkReply::OperationCanceledError: //this means I closed it myself by abort() or close() //I don't call them directory, but this is the error code //Qt returns when it can not resume donwload after the network failure //or when the download is canceled by the timout; errorText = "Connection lost"; break; case QNetworkReply::SslHandshakeFailedError: errorText = "Security error"; //TODO need to handle sslErrors signal to get a better description here break; case QNetworkReply::TemporaryNetworkFailureError: //this means the connection is lost by opened route, but it's going to be resumed, not sure I need to notify break; case QNetworkReply::NetworkSessionFailedError: errorText = "Outgoing connection problem"; break; case QNetworkReply::BackgroundRequestNotAllowedError: errorText = "Background request is not allowed"; break; case QNetworkReply::TooManyRedirectsError: errorText = "The request was redirected too many times"; break; case QNetworkReply::InsecureRedirectError: errorText = "The request was redirected to insecure connection"; break; case QNetworkReply::UnknownNetworkError: errorText = "Unknown network error"; break; // proxy errors (101-199): case QNetworkReply::ProxyConnectionRefusedError: errorText = "The connection to the proxy server was refused"; break; case QNetworkReply::ProxyConnectionClosedError: errorText = "Proxy server closed the connection"; break; case QNetworkReply::ProxyNotFoundError: errorText = "Proxy host was not found"; break; case QNetworkReply::ProxyTimeoutError: errorText = "Connection to the proxy server was closed because it timed out"; break; case QNetworkReply::ProxyAuthenticationRequiredError: errorText = "Couldn't connect to proxy server, authentication is required"; break; case QNetworkReply::UnknownProxyError: errorText = "Unknown proxy error"; break; // content errors (201-299): case QNetworkReply::ContentAccessDenied: errorText = "The access to file is denied"; break; case QNetworkReply::ContentOperationNotPermittedError: errorText = "The operation over requesting file is not permitted"; break; case QNetworkReply::ContentNotFoundError: errorText = "The file was not found"; break; case QNetworkReply::AuthenticationRequiredError: errorText = "Couldn't access the file, authentication is required"; break; case QNetworkReply::ContentReSendError: errorText = "Sending error, one more attempt will probably solve this problem"; break; case QNetworkReply::ContentConflictError: errorText = "The request could not be completed due to a conflict with the current state of the resource"; break; case QNetworkReply::ContentGoneError: errorText = "The requested resource is no longer available at the server"; break; case QNetworkReply::UnknownContentError: errorText = "Unknown content error"; break; // protocol errors case QNetworkReply::ProtocolUnknownError: errorText = "Unknown protocol error"; break; case QNetworkReply::ProtocolInvalidOperationError: errorText = "Requested operation is not permitted in this protocol"; break; case QNetworkReply::ProtocolFailure: errorText = "Low level protocol error"; break; // Server side errors (401-499) case QNetworkReply::InternalServerError: errorText = "Internal server error"; break; case QNetworkReply::OperationNotImplementedError: errorText = "Server doesn't support requested operation"; break; case QNetworkReply::ServiceUnavailableError: errorText = "The server is not available for this operation right now"; break; case QNetworkReply::UnknownServerError: errorText = "Unknown server error"; break; } return errorText; } void Core::NetworkAccess::onDownloadFinished() { qDebug() << "DEBUG: DOWNLOAD FINISHED"; QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = downloads.find(url); if (itr == downloads.end()) { qDebug() << "an error downloading" << url << ": the request is done but there is no record of it being downloaded, ignoring"; } else { Transfer* dwn = itr->second; if (dwn->success) { qDebug() << "download success for" << url; QString err; QStringList hops = url.split("/"); QString fileName = hops.back(); QString jid; if (dwn->messages.size() > 0) jid = dwn->messages.front().jid; else qDebug() << "An attempt to save the file but it doesn't seem to belong to any message, download is definately going to be broken"; QString path = prepareDirectory(jid); if (path.size() > 0) { path = checkFileName(fileName, path); QFile file(Shared::resolvePath(path)); if (file.open(QIODevice::WriteOnly)) { file.write(dwn->reply->readAll()); file.close(); storage.setPath(url, path); qDebug() << "file" << path << "was successfully downloaded"; } else { qDebug() << "couldn't save file" << path; err = "Error opening file to write:" + file.errorString(); } } else { err = "Couldn't prepare a directory for file"; } if (path.size() > 0) emit downloadFileComplete(dwn->messages, path); else emit loadFileError(dwn->messages, "Error saving file " + url + "; " + err, false); } dwn->reply->deleteLater(); delete dwn; downloads.erase(itr); } } void Core::NetworkAccess::startDownload(const std::list& msgs, const QString& url) { Transfer* dwn = new Transfer({msgs, 0, 0, true, "", url, 0}); QNetworkRequest req(url); dwn->reply = manager->get(req); connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress); connect(dwn->reply, &QNetworkReply::sslErrors, this, &NetworkAccess::onDownloadSSLError); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(dwn->reply, qOverload(&QNetworkReply::errorOccurred), this, &NetworkAccess::onDownloadError); #else connect(dwn->reply, qOverload(&QNetworkReply::error), this, &NetworkAccess::onDownloadError); #endif connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onDownloadFinished); downloads.insert(std::make_pair(url, dwn)); emit loadFileProgress(dwn->messages, 0, false); } void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code) { QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == uploads.end()) { qDebug() << "an error uploading" << url << ": the request is reporting an error but there is no record of it being uploading, ignoring"; } else { QString errorText = getErrorText(code); //if (errorText.size() > 0) { itr->second->success = false; Transfer* upl = itr->second; emit loadFileError(upl->messages, errorText, true); //} //TODO deletion? } } void Core::NetworkAccess::onUploadFinished() { QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == downloads.end()) { qDebug() << "an error uploading" << url << ": the request is done there is no record of it being uploading, ignoring"; } else { Transfer* upl = itr->second; if (upl->success) { qDebug() << "upload success for" << url; // Copy file to Download folder if it is a temp file. See Conversation::onImagePasted. if (upl->path.startsWith(QDir::tempPath() + QDir::separator() + QStringLiteral("squawk_img_attach_")) && upl->path.endsWith(".png")) { QString err = ""; QString downloadDirPath = prepareDirectory(upl->messages.front().jid); if (downloadDirPath.size() > 0) { QString newPath = downloadDirPath + QDir::separator() + upl->path.mid(QDir::tempPath().length() + 1); // Copy {TEMPDIR}/squawk_img_attach_XXXXXX.png to Download folder bool copyResult = QFile::copy(upl->path, Shared::resolvePath(newPath)); if (copyResult) upl->path = newPath; // Change storage else err = "copying to " + newPath + " failed"; } else { err = "Couldn't prepare a directory for file"; } if (err.size() != 0) qDebug() << "failed to copy temporary upload file " << upl->path << " to download folder:" << err; } storage.addFile(upl->messages, upl->url, upl->path); emit uploadFileComplete(upl->messages, upl->url, upl->path); } upl->reply->deleteLater(); upl->file->close(); upl->file->deleteLater(); delete upl; uploads.erase(itr); } } void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTotal) { QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == uploads.end()) { qDebug() << "an error downloading" << url << ": the request had some progress but seems like no one is waiting for it, skipping"; } else { Transfer* upl = itr->second; if (upl->success) { qreal received = bytesReceived; qreal total = bytesTotal; qreal progress = received/total; upl->progress = progress; emit loadFileProgress(upl->messages, progress, true); } } } QString Core::NetworkAccess::getFileRemoteUrl(const QString& path) { QString p = Shared::squawkifyPath(path); try { p = storage.getUrl(p); } catch (const LMDBAL::NotFound& err) { p = ""; } catch (...) { throw; } return p; } void Core::NetworkAccess::uploadFile( const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap headers ) { QFile* file = new QFile(path); Transfer* upl = new Transfer({{info}, 0, 0, true, path, get.toString(), file}); QNetworkRequest req(put); for (QMap::const_iterator itr = headers.begin(), end = headers.end(); itr != end; itr++) { req.setRawHeader(itr.key().toUtf8(), itr.value().toUtf8()); } if (file->open(QIODevice::ReadOnly)) { upl->reply = manager->put(req, file); connect(upl->reply, &QNetworkReply::uploadProgress, this, &NetworkAccess::onUploadProgress); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(upl->reply, qOverload(&QNetworkReply::errorOccurred), this, &NetworkAccess::onUploadError); #else connect(upl->reply, qOverload(&QNetworkReply::error), this, &NetworkAccess::onUploadError); #endif connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished); uploads.insert(std::make_pair(put.toString(), upl)); emit loadFileProgress(upl->messages, 0, true); } else { qDebug() << "couldn't upload file" << path; emit loadFileError(upl->messages, "Error opening file", true); delete file; delete upl; } } void Core::NetworkAccess::registerFile(const QString& url, const QString& account, const QString& jid, const QString& id) { storage.addFile(url, account, jid, id); std::map::iterator itr = downloads.find(url); if (itr != downloads.end()) itr->second->messages.emplace_back(account, jid, id); //TODO notification is going to happen the next tick, is that okay? } void Core::NetworkAccess::registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id) { storage.addFile(url, path, account, jid, id); } bool Core::NetworkAccess::checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path) { for (const std::pair& pair : uploads) { Transfer* info = pair.second; if (pair.second->path == path) { std::list& messages = info->messages; bool dup = false; for (const Shared::MessageInfo& info : messages) { if (info.account == acc && info.jid == jid && info.messageId == id) { dup = true; break; } } if (!dup) { info->messages.emplace_back(acc, jid, id); //TODO notification is going to happen the next tick, is that okay? return true; } } } return false; } QString Core::NetworkAccess::prepareDirectory(const QString& jid) { QString path = currentPath; QString addition; if (jid.size() > 0) { addition = jid; path += QDir::separator() + jid; } QDir location(path); if (!location.exists()) { bool res = location.mkpath(path); if (!res) return ""; else return "squawk://" + addition; } return "squawk://" + addition; } QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path) { QStringList parts = name.split("."); QString suffix(""); QStringList::const_iterator sItr = parts.begin(); QString realName = *sItr; ++sItr; for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) suffix += "." + (*sItr); QString postfix(""); QString resolvedPath = Shared::resolvePath(path); QString count(""); QFileInfo proposedName(resolvedPath + QDir::separator() + realName + count + suffix); int counter = 0; while (proposedName.exists()) { count = QString("(") + std::to_string(++counter).c_str() + ")"; proposedName = QFileInfo(resolvedPath + QDir::separator() + realName + count + suffix); } return path + QDir::separator() + realName + count + suffix; } QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) { return storage.addMessageAndCheckForPath(url, account, jid, id); } std::list Core::NetworkAccess::reportPathInvalid(const QString& path) { return storage.deletedFile(path); } void Core::NetworkAccess::moveFilesDirectory(const QString& newPath) { QDir dir(currentPath); bool success = true; qDebug() << "moving" << currentPath << "to" << newPath; for (QString fileName : dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) success = dir.rename(fileName, newPath + QDir::separator() + fileName) && success; if (!success) qDebug() << "couldn't move downloads directory, most probably downloads will be broken"; currentPath = newPath; }