diff --git a/src/dcc/transferrecv.cpp b/src/dcc/transferrecv.cpp index 8e6da649..5d424854 100644 --- a/src/dcc/transferrecv.cpp +++ b/src/dcc/transferrecv.cpp @@ -1,966 +1,966 @@ /* receive a file on DCC protocol begin: Mit Aug 7 2002 copyright: (C) 2002 by Dario Abatianni email: eisfuchs@tigress.com */ /* Copyright (C) 2004-2007 Shintaro Matsuoka Copyright (C) 2004,2005 John Tapsell Copyright (C) 2009 Michael Kreitzer Copyright (C) 2009 Bernd Buschinski */ /* 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 2 of the License, or (at your option) any later version. */ #include #ifdef Q_OS_WIN // Prevent windows system header files from defining min/max as macros. #define NOMINMAX 1 #include #endif #include "transferrecv.h" #include "dcccommon.h" #include "transfermanager.h" #include "application.h" #include "connectionmanager.h" #include "server.h" #include "upnprouter.h" #include #include #include #include #include #include #include /* *flow chart* TransferRecv() start() : called from TransferPanel when user pushes the accept button | \ | requestResume() : called when user chooses to resume in ResumeDialog. it emits the signal ResumeRequest() | | startResume() : called by "Server" | | connectToSender() connectionSuccess() : called by recvSocket */ namespace Konversation { namespace DCC { TransferRecv::TransferRecv(QObject *parent) : Transfer(Transfer::Receive, parent) { qDebug(); m_serverSocket = nullptr; m_recvSocket = nullptr; m_writeCacheHandler = nullptr; m_connectionTimer = new QTimer(this); m_connectionTimer->setSingleShot(true); connect(m_connectionTimer, &QTimer::timeout, this, &TransferRecv::connectionTimeout); //timer hasn't started yet. qtimer will be deleted automatically when 'this' object is deleted } TransferRecv::~TransferRecv() { qDebug(); cleanUp(); } void TransferRecv::cleanUp() { qDebug(); stopConnectionTimer(); disconnect(m_connectionTimer, nullptr, nullptr, nullptr); finishTransferLogger(); if (m_serverSocket) { m_serverSocket->close(); m_serverSocket = nullptr; if (m_reverse && Preferences::self()->dccUPnP()) { UPnP::UPnPRouter *router = Application::instance()->getDccTransferManager()->getUPnPRouter(); if (router) { router->undoForward(m_ownPort, QAbstractSocket::TcpSocket); } } } if (m_recvSocket) { disconnect(m_recvSocket, nullptr, nullptr, nullptr); m_recvSocket->close(); m_recvSocket = nullptr; // the instance will be deleted automatically by its parent } if (m_writeCacheHandler) { m_writeCacheHandler->closeNow(); m_writeCacheHandler->deleteLater(); m_writeCacheHandler = nullptr; } Transfer::cleanUp(); } void TransferRecv::setPartnerIp(const QString &ip) { if (getStatus() == Configuring) { m_partnerIp = ip; } } void TransferRecv::setPartnerPort(quint16 port) { if (getStatus() == Configuring) { m_partnerPort = port; } } void TransferRecv::setFileSize(quint64 fileSize) { if (getStatus() == Configuring) { m_fileSize = fileSize; } } void TransferRecv::setFileName(const QString &fileName) { if (getStatus() == Configuring) { m_fileName = fileName; m_saveFileName = m_fileName; } } void TransferRecv::setFileURL(const QUrl &url) { if (getStatus() == Preparing || getStatus() == Configuring || getStatus() == Queued) { m_fileURL = url; m_saveFileName = url.fileName(); } } void TransferRecv::setReverse(bool reverse, const QString &reverseToken) { if (getStatus() == Configuring) { m_reverse = reverse; if (reverse) { m_partnerPort = 0; m_reverseToken = reverseToken; } } } bool TransferRecv::queue() { qDebug(); if (getStatus() != Configuring) { return false; } if (m_partnerIp.isEmpty()) { return false; } if (m_ownIp.isEmpty()) { m_ownIp = DccCommon::getOwnIp(Application::instance()->getConnectionManager()->getServerByConnectionId(m_connectionId)); } if (!KAuthorized::authorizeAction(QStringLiteral("allow_downloading"))) { //note we have this after the initialisations so that item looks okay //Do not have the rights to send the file. Shouldn't have gotten this far anyway failed(i18n("The admin has restricted the right to receive files")); return false; } // check if the sender IP is valid if (m_partnerIp == QLatin1String("0.0.0.0")) { failed(i18n("Invalid sender address (%1)", m_partnerIp)); return false; } // TODO: should we support it? if (m_fileSize == 0) { failed(i18n("Unsupported negotiation (filesize=0)")); return false; } if (m_fileName.isEmpty()) { m_fileName = "unnamed_file_" + QDateTime::currentDateTime().toString(Qt::ISODate).remove(':'); m_saveFileName = m_fileName; } if (m_fileURL.isEmpty()) { // determine default incoming file URL // set default folder if (!Preferences::self()->dccPath().isEmpty()) { m_fileURL = Preferences::self()->dccPath(); } else { m_fileURL.setPath(KUser(KUser::UseRealUserID).homeDir()); // default folder is *not* specified } //buschinski TODO CHECK ME // add a slash if there is none //m_fileURL.adjustPath(KUrl::AddTrailingSlash); // Append folder with partner's name if wanted if (Preferences::self()->dccCreateFolder()) { m_fileURL = m_fileURL.adjusted(QUrl::StripTrailingSlash); m_fileURL.setPath(m_fileURL.path() + QDir::separator() + m_partnerNick); } // Just incase anyone tries to do anything nasty QString fileNameSanitized = sanitizeFileName(m_saveFileName); // Append partner's name to file name if wanted if (Preferences::self()->dccAddPartner()) { m_fileURL = m_fileURL.adjusted(QUrl::StripTrailingSlash); m_fileURL.setPath(m_fileURL.path() + QDir::separator() + m_partnerNick + '.' + fileNameSanitized); } else { m_fileURL = m_fileURL.adjusted(QUrl::StripTrailingSlash); m_fileURL.setPath(m_fileURL.path() + QDir::separator() + fileNameSanitized); } } return Transfer::queue(); } void TransferRecv::abort() // public slot { qDebug(); if (getStatus() == Transfer::Queued) { Server *server = Application::instance()->getConnectionManager()->getServerByConnectionId(m_connectionId); if (server) { server->dccRejectSend(m_partnerNick, transferFileName(m_fileName)); } } if(m_writeCacheHandler) { m_writeCacheHandler->write(true); // flush } cleanUp(); setStatus(Aborted); emit done(this); } void TransferRecv::start() // public slot { qDebug() << "[BEGIN]"; if (getStatus() != Queued) { return; } setStatus(Preparing); prepareLocalKio(false, false); qDebug() << "[END]"; } void TransferRecv::prepareLocalKio(bool overwrite, bool resume, KIO::fileoffset_t startPosition) { qDebug() << "URL: " << m_fileURL << endl << "Overwrite: " << overwrite << endl << "Resume: " << resume << " (Position: " << startPosition << ")"; m_resumed = resume; m_transferringPosition = startPosition; if (!createDirs(KIO::upUrl(m_fileURL))) { askAndPrepareLocalKio(i18n("Cannot create the folder or destination is not writable.
" "Folder: %1
", KIO::upUrl(m_fileURL).toString()), ResumeDialog::RA_Rename | ResumeDialog::RA_Cancel | ResumeDialog::RA_OverwriteDefaultPath, ResumeDialog::RA_Rename); return; } if (Application::instance()->getDccTransferManager()->isLocalFileInWritingProcess(m_fileURL)) { askAndPrepareLocalKio(i18n("The file is used by another transfer.
" "%1
", m_fileURL.toString()), ResumeDialog::RA_Rename | ResumeDialog::RA_Cancel, ResumeDialog::RA_Rename); return; } KIO::JobFlags flags; if(overwrite) { flags |= KIO::Overwrite; } if(m_resumed) { flags |= KIO::Resume; } //for now, maybe later flags |= KIO::HideProgressInfo; KIO::TransferJob *transferJob = KIO::put(m_fileURL, -1, flags); if (!transferJob) { qDebug() << "KIO::put() returned NULL. what happened?"; failed(i18n("Could not create a KIO instance")); return; } transferJob->setAutoDelete(true); connect(transferJob, &KIO::TransferJob::canResume, this, &TransferRecv::slotLocalCanResume); connect(transferJob, &KIO::TransferJob::result, this, &TransferRecv::slotLocalGotResult); connect(transferJob, &KIO::TransferJob::dataReq, this, &TransferRecv::slotLocalReady); } void TransferRecv::askAndPrepareLocalKio(const QString &message, int enabledActions, ResumeDialog::ReceiveAction defaultAction, KIO::fileoffset_t startPosition) { switch (ResumeDialog::ask(this, message, enabledActions, defaultAction)) { case ResumeDialog::RA_Resume: prepareLocalKio(false, true, startPosition); break; case ResumeDialog::RA_Overwrite: prepareLocalKio(true, false); break; case ResumeDialog::RA_Rename: prepareLocalKio(false, false); break; case ResumeDialog::RA_Cancel: default: setStatus(Queued); } } bool TransferRecv::createDirs(const QUrl &dirURL) const { QUrl kurl(dirURL); //First we split directories until we reach to the top, //since we need to create directories one by one QList dirList; while (kurl != KIO::upUrl(kurl)) { dirList.prepend(kurl); kurl = KIO::upUrl(kurl); } //Now we create the directories QList::ConstIterator it; for (it=dirList.constBegin(); it != dirList.constEnd(); ++it) { KIO::StatJob* statJob = KIO::stat(*it, KIO::StatJob::SourceSide, 0); statJob->exec(); if (statJob->error()) { KIO::MkdirJob* job = KIO::mkdir(*it, -1); if (!job->exec()) { return false; } } } #ifndef Q_OS_WIN QFileInfo dirInfo(dirURL.toLocalFile()); if (!dirInfo.isWritable()) { return false; } #else //!TODO find equivalent windows solution //from 4.7 QFile Doc: // File permissions are handled differently on Linux/Mac OS X and Windows. // In a non writable directory on Linux, files cannot be created. // This is not always the case on Windows, where, for instance, // the 'My Documents' directory usually is not writable, but it is still // possible to create files in it. #endif return true; } void TransferRecv::slotLocalCanResume(KIO::Job *job, KIO::filesize_t size) { qDebug() << "[BEGIN]" << endl << "size: " << size; KIO::TransferJob* transferJob = dynamic_cast(job); if (!transferJob) { qDebug() << "not a TransferJob? returning"; return; } if (size != 0) { disconnect(transferJob, nullptr, nullptr, nullptr); if (Preferences::self()->dccAutoResume()) { prepareLocalKio(false, true, size); } else { askAndPrepareLocalKio(i18np( "A partial file exists:
" "%2
" "Size of the partial file: 1 byte.
", "A partial file exists:
" "%2
" "Size of the partial file: %1 bytes.
", size, m_fileURL.toString()), ResumeDialog::RA_Resume | ResumeDialog::RA_Overwrite | ResumeDialog::RA_Rename | ResumeDialog::RA_Cancel, ResumeDialog::RA_Resume, size); } transferJob->putOnHold(); } qDebug() << "[END]"; } void TransferRecv::slotLocalGotResult(KJob *job) { qDebug() << "[BEGIN]"; KIO::TransferJob* transferJob = qobject_cast(job); disconnect(transferJob, nullptr, nullptr, nullptr); switch (transferJob->error()) { case 0: // no error - qDebug() << "job->error() returned 0." << endl + qDebug() << "job->error() returned 0.\n" << "Why was I called in spite of no error?"; break; case KIO::ERR_FILE_ALREADY_EXIST: askAndPrepareLocalKio(i18nc("%1=fileName, %2=local filesize, %3=sender filesize", "The file already exists.
" "%1 (%2)
" "Sender reports file size of %3
", m_fileURL.toString(), KIO::convertSize(QFileInfo(m_fileURL.path()).size()), KIO::convertSize(m_fileSize)), ResumeDialog::RA_Overwrite | ResumeDialog::RA_Rename | ResumeDialog::RA_Cancel, ResumeDialog::RA_Overwrite); break; default: askAndPrepareLocalKio(i18n("Could not open the file.
" "Error: %1

" "%2
", transferJob->error(), m_fileURL.toString()), ResumeDialog::RA_Rename | ResumeDialog::RA_Cancel, ResumeDialog::RA_Rename); } qDebug() << "[END]"; } void TransferRecv::slotLocalReady(KIO::Job *job) { qDebug(); KIO::TransferJob* transferJob = qobject_cast(job); disconnect(transferJob, nullptr, nullptr, nullptr); // WriteCacheHandler will control the job after this m_writeCacheHandler = new TransferRecvWriteCacheHandler(transferJob); connect(m_writeCacheHandler, &TransferRecvWriteCacheHandler::done, this, &TransferRecv::slotLocalWriteDone); connect(m_writeCacheHandler, &TransferRecvWriteCacheHandler::gotError, this, &TransferRecv::slotLocalGotWriteError); if (!m_resumed) { connectWithSender(); } else { requestResume(); } } void TransferRecv::connectWithSender() { if (m_reverse) { if (!startListeningForSender()) { return; } Server *server = Application::instance()->getConnectionManager()->getServerByConnectionId(m_connectionId); if (!server) { failed(i18n("Could not send Reverse DCC SEND acknowledgement to the partner via the IRC server.")); return; } m_ownIp = DccCommon::getOwnIp(server); m_ownPort = m_serverSocket->serverPort(); if (Preferences::self()->dccUPnP()) { UPnP::UPnPRouter *router = Application::instance()->getDccTransferManager()->getUPnPRouter(); if (router && router->forward(QHostAddress(server->getOwnIpByNetworkInterface()), m_ownPort, QAbstractSocket::TcpSocket)) { connect(router, &UPnP::UPnPRouter::forwardComplete, this, &TransferRecv::sendReverseAck); } else { sendReverseAck(true, 0); // Try anyways on error } } else { sendReverseAck(false, 0); } } else { connectToSendServer(); } } void TransferRecv::sendReverseAck(bool error, quint16 port) { Server *server = Application::instance()->getConnectionManager()->getServerByConnectionId(m_connectionId); if (!server) { failed(i18n("Could not send Reverse DCC SEND acknowledgement to the partner via the IRC server.")); return; } qDebug(); if (Preferences::self()->dccUPnP() && this->sender()) { if (port != m_ownPort) return; // Somebody elses forward succeeded disconnect (this->sender(), SIGNAL(forwardComplete(bool,quint16)), this, SLOT(sendRequest(bool,quint16))); if (error) { server->appendMessageToFrontmost(i18nc("Universal Plug and Play", "UPnP"), i18n("Failed to forward port %1. Sending DCC request to remote user regardless.", QString::number(m_ownPort)), QHash(), false); } } setStatus(WaitingRemote, i18n("Waiting for connection")); server->dccReverseSendAck(m_partnerNick, transferFileName(m_fileName), DccCommon::textIpToNumericalIp(m_ownIp), m_ownPort, m_fileSize, m_reverseToken); } void TransferRecv::requestResume() { qDebug(); setStatus(WaitingRemote, i18n("Waiting for remote host's acceptance")); startConnectionTimer(30); qDebug() << "Requesting resume for " << m_partnerNick << " file " << m_fileName << " partner " << m_partnerPort; Server *server = Application::instance()->getConnectionManager()->getServerByConnectionId(m_connectionId); if (!server) { qDebug() << "Could not retrieve the instance of Server. Connection id: " << m_connectionId; failed(i18n("Could not send DCC RECV resume request to the partner via the IRC server.")); return; } if (m_reverse) { server->dccPassiveResumeGetRequest(m_partnerNick, transferFileName(m_fileName), m_partnerPort, m_transferringPosition, m_reverseToken); } else { server->dccResumeGetRequest(m_partnerNick, transferFileName(m_fileName), m_partnerPort, m_transferringPosition); } } // public slot void TransferRecv::startResume(quint64 position) { qDebug() << "Position:" << position; stopConnectionTimer(); if ((quint64)m_transferringPosition != position) { qDebug() << "remote responded with an unexpected position"<< endl << "expected: " << m_transferringPosition << endl << "remote response: " << position; failed(i18n("Unexpected response from remote host")); return; } connectWithSender(); } void TransferRecv::connectToSendServer() { qDebug(); // connect to sender setStatus(Connecting); startConnectionTimer(30); m_recvSocket = new QTcpSocket(this); connect(m_recvSocket, &QTcpSocket::connected, this, &TransferRecv::startReceiving); connect(m_recvSocket, static_cast(&QTcpSocket::error), this, &TransferRecv::connectionFailed); qDebug() << "Attempting to connect to " << m_partnerIp << ":" << m_partnerPort; m_recvSocket->connectToHost(m_partnerIp, m_partnerPort); } bool TransferRecv::startListeningForSender() { // Set up server socket QString failedReason; if (Preferences::self()->dccSpecificSendPorts()) { m_serverSocket = DccCommon::createServerSocketAndListen(this, &failedReason, Preferences::self()->dccSendPortsFirst(), Preferences::self()->dccSendPortsLast()); } else { m_serverSocket = DccCommon::createServerSocketAndListen(this, &failedReason); } if (!m_serverSocket) { failed(failedReason); return false; } connect(m_serverSocket, &QTcpServer::newConnection, this, &TransferRecv::slotServerSocketReadyAccept); startConnectionTimer(30); return true; } void TransferRecv::slotServerSocketReadyAccept() { //reverse dcc m_recvSocket = m_serverSocket->nextPendingConnection(); if (!m_recvSocket) { failed(i18n("Could not accept the connection (socket error).")); return; } connect(m_recvSocket, static_cast(&QTcpSocket::error), this, &TransferRecv::connectionFailed); // we don't need ServerSocket anymore m_serverSocket->close(); m_serverSocket = nullptr; // Will be deleted by parent if (Preferences::self()->dccUPnP()) { UPnP::UPnPRouter *router = Application::instance()->getDccTransferManager()->getUPnPRouter(); if (router) { router->undoForward(m_ownPort, QAbstractSocket::TcpSocket); } } startReceiving(); } void TransferRecv::startReceiving() { qDebug(); stopConnectionTimer(); connect(m_recvSocket, &QTcpSocket::readyRead, this, &TransferRecv::readData); m_transferStartPosition = m_transferringPosition; //we don't need the original filename anymore, overwrite it to display the correct one in transfermanager/panel m_fileName = m_saveFileName; m_ownPort = m_recvSocket->localPort(); startTransferLogger(); // initialize CPS counter, ETA counter, etc... setStatus(Transferring); } // slot void TransferRecv::connectionFailed(QAbstractSocket::SocketError errorCode) { qDebug() << "Code = " << errorCode << ", string = " << m_recvSocket->errorString(); failed(m_recvSocket->errorString()); } void TransferRecv::readData() // slot { //qDebug(); qint64 actual = m_recvSocket->read(m_buffer, m_bufferSize); if (actual > 0) { //actual is the size we read in, and is guaranteed to be less than m_bufferSize m_transferringPosition += actual; m_writeCacheHandler->append(m_buffer, actual); m_writeCacheHandler->write(false); //in case we could not read all the data, leftover data could get lost if (m_recvSocket->bytesAvailable() > 0) { readData(); } else { sendAck(); } } } void TransferRecv::sendAck() // slot { //qDebug() << m_transferringPosition << "/" << (KIO::fileoffset_t)m_fileSize; //It is bound to be 32bit according to dcc specs, -> 4GB limit. //But luckily no client ever reads this value, //except for old mIRC versions, but they couldn't send or receive files over 4GB anyway. //Note: The resume and filesize are set via dcc send command and can be over 4GB quint32 pos = htonl(static_cast(m_transferringPosition)); m_recvSocket->write((char*)&pos, 4); if (m_transferringPosition == static_cast(m_fileSize)) { qDebug() << "Sent final ACK."; disconnect(m_recvSocket, nullptr, nullptr, nullptr); m_writeCacheHandler->close(); // WriteCacheHandler will send the signal done() } else if (m_transferringPosition > static_cast(m_fileSize)) { qDebug() << "The remote host sent larger data than expected: " << m_transferringPosition; failed(i18n("Transfer error")); } } void TransferRecv::slotLocalWriteDone() // <-WriteCacheHandler::done() { qDebug(); cleanUp(); setStatus(Done); emit done(this); } // <- WriteCacheHandler::gotError() void TransferRecv::slotLocalGotWriteError(const QString &errorString) { qDebug(); failed(i18n("KIO error: %1", errorString)); } void TransferRecv::startConnectionTimer(int secs) { qDebug(); m_connectionTimer->start(secs * 1000); } void TransferRecv::stopConnectionTimer() { if (m_connectionTimer->isActive()) { m_connectionTimer->stop(); qDebug(); } } void TransferRecv::connectionTimeout() // slot { qDebug(); failed(i18n("Timed out")); } // WriteCacheHandler TransferRecvWriteCacheHandler::TransferRecvWriteCacheHandler(KIO::TransferJob *transferJob) : m_transferJob(transferJob) { m_writeReady = true; m_cacheStream = nullptr; connect(m_transferJob, &KIO::TransferJob::dataReq, this, &TransferRecvWriteCacheHandler::slotKIODataReq); connect(m_transferJob, &KIO::TransferJob::result, this, &TransferRecvWriteCacheHandler::slotKIOResult); m_transferJob->setAsyncDataEnabled(m_writeAsyncMode = true); } TransferRecvWriteCacheHandler::~TransferRecvWriteCacheHandler() { closeNow(); } // public void TransferRecvWriteCacheHandler::append(char *data, int size) { // sendAsyncData() and dataReq() cost a lot of time, so we should pack some caches. static const int maxWritePacketSize = 1 * 1024 * 1024; // 1meg if (m_cacheList.isEmpty() || m_cacheList.back().size() + size > maxWritePacketSize) { m_cacheList.append(QByteArray()); delete m_cacheStream; m_cacheStream = new QDataStream(&m_cacheList.back(), QIODevice::WriteOnly); } m_cacheStream->writeRawData(data, size); } // public bool TransferRecvWriteCacheHandler::write(bool force) { // force == false: return without doing anything when the whole cache size is smaller than maxWritePacketSize if (m_cacheList.isEmpty() || !m_transferJob || !m_writeReady || !m_writeAsyncMode) { return false; } if (!force && m_cacheList.count() < 2) { return false; } // do write m_writeReady = false; m_transferJob->sendAsyncData(m_cacheList.front()); //qDebug() << "wrote " << m_cacheList.front().size() << " bytes."; m_cacheList.pop_front(); return true; } void TransferRecvWriteCacheHandler::close() // public { qDebug(); write(true); // write once if kio is ready to write m_transferJob->setAsyncDataEnabled(m_writeAsyncMode = false); qDebug() << "switched to synchronized mode."; qDebug() << "flushing... (remaining caches: " << m_cacheList.count() << ")"; } void TransferRecvWriteCacheHandler::closeNow() // public { write(true); // flush if (m_transferJob) { m_transferJob->kill(); m_transferJob = nullptr; } m_cacheList.clear(); delete m_cacheStream; m_cacheStream = nullptr; } void TransferRecvWriteCacheHandler::slotKIODataReq(KIO::Job *job, QByteArray &data) { Q_UNUSED(job); // We are in writeAsyncMode if there is more data to be read in from dcc if (m_writeAsyncMode) { m_writeReady = true; } else { // No more data left to read from incoming dcctransfer if (!m_cacheList.isEmpty()) { // once we write everything in cache, the file is complete. // This function will be called once more after this last data is written. data = m_cacheList.front(); qDebug() << "will write " << m_cacheList.front().size() << " bytes."; m_cacheList.pop_front(); } else { // finally, no data left to write or read. qDebug() << "flushing done."; m_transferJob = nullptr; emit done(); // -> TransferRecv::slotLocalWriteDone() } } } void TransferRecvWriteCacheHandler::slotKIOResult(KJob *job) { Q_ASSERT(m_transferJob); disconnect(m_transferJob, nullptr, nullptr, nullptr); m_transferJob = nullptr; if (job->error()) { QString errorString = job->errorString(); closeNow(); emit gotError(errorString); // -> TransferRecv::slotLocalGotWriteError() } } } } diff --git a/src/upnp/upnpmcastsocket.cpp b/src/upnp/upnpmcastsocket.cpp index bf647400..7eee52d1 100644 --- a/src/upnp/upnpmcastsocket.cpp +++ b/src/upnp/upnpmcastsocket.cpp @@ -1,242 +1,242 @@ /* 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 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2005-2007 Joris Guisson Copyright (C) 2009 Michael Kreitzer */ #include "upnpmcastsocket.h" #include #include #include #ifdef Q_OS_WIN #include #else #include #include #include #include #include #include #endif namespace Konversation { namespace UPnP { UPnPMCastSocket::UPnPMCastSocket() : QUdpSocket () { QObject::connect(this, &UPnPMCastSocket::readyRead, this, &UPnPMCastSocket::onReadyRead); QObject::connect(this, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError))); for (quint32 i = 0;i < 10;i++) { if (!bind(1900 + i,QUdpSocket::ShareAddress)) qDebug() << "Cannot bind to UDP port 1900 : " << errorString() << endl; else break; } joinUPnPMCastGroup(); } UPnPMCastSocket::~UPnPMCastSocket() { qDeleteAll(pending_routers); qDeleteAll(routers); leaveUPnPMCastGroup(); } void UPnPMCastSocket::discover() { qDebug() << "Trying to find UPnP devices on the local network" << endl; // send a HTTP M-SEARCH message to 239.255.255.250:1900 const char* data = "M-SEARCH * HTTP/1.1\r\n" "HOST: 239.255.255.250:1900\r\n" "ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" "MAN:\"ssdp:discover\"\r\n" "MX:3\r\n" "\r\n\0"; writeDatagram(data,strlen(data),QHostAddress(QStringLiteral("239.255.255.250")),1900); } void UPnPMCastSocket::onXmlFileDownloaded(UPnPRouter* r,bool success) { pending_routers.remove(r); if (!success) { // we couldn't download and parse the XML file so // get rid of it r->deleteLater(); } else { // add it to the list and emit the signal if (!routers.contains(r->getUUID())) { routers.insert(r->getUUID(),r); emit discovered(r); } else { r->deleteLater(); } } } void UPnPMCastSocket::onReadyRead() { QByteArray data(pendingDatagramSize(),0); if (readDatagram(data.data(),pendingDatagramSize()) == -1) return; // try to make a router of it UPnPRouter* r = parseResponse(data); if (r) { QObject::connect(r,&UPnPRouter::xmlFileDownloaded, this,&UPnPMCastSocket::onXmlFileDownloaded); // download it's xml file r->downloadXMLFile(); pending_routers.insert(r); } } UPnPRouter* UPnPMCastSocket::parseResponse(const QByteArray & arr) { QStringList lines = QString::fromLatin1(arr).split(QStringLiteral("\r\n")); QString server; QUrl location; QString uuid; // first read first line and see if contains a HTTP 200 OK message QString line = lines.first(); if (line.contains(QLatin1String("HTTP"))) { // it is either a 200 OK or a NOTIFY if (!line.contains(QLatin1String("NOTIFY")) && !line.contains(QLatin1String("200 OK"))) return nullptr; } else return nullptr; // quick check that the response being parsed is valid bool validDevice = false; for (int idx = 0;idx < lines.count() && !validDevice; idx++) { line = lines[idx]; if ((line.contains(QLatin1String("ST:")) || line.contains(QLatin1String("NT:"))) && line.contains(QLatin1String("InternetGatewayDevice"))) { validDevice = true; } } if (!validDevice) { // qDebug() << "Not a valid Internet Gateway Device" << endl; return nullptr; } // read all lines and try to find the server and location fields for (int i = 1;i < lines.count();i++) { line = lines[i]; if (line.startsWith(QLatin1String("Location"), Qt::CaseInsensitive)) { location = QUrl(line.mid(line.indexOf(':') + 1).trimmed()); if (!location.isValid()) return nullptr; } else if (line.startsWith(QLatin1String("Server"), Qt::CaseInsensitive)) { server = line.mid(line.indexOf(':') + 1).trimmed(); if (server.length() == 0) return nullptr; } else if (line.contains(QLatin1String("USN"), Qt::CaseInsensitive) && line.contains(QLatin1String("uuid"), Qt::CaseInsensitive)) { uuid = line.split(':').at(2); if (uuid.length() == 0) return nullptr; } } if (routers.contains(uuid)) { return nullptr; } else { qDebug() << "Detected IGD " << server << "UUID" << uuid << endl; // everything OK, make a new UPnPRouter return new UPnPRouter(server,location,uuid); } } void UPnPMCastSocket::onError(QAbstractSocket::SocketError ) { qDebug() << "UPnPMCastSocket Error : " << errorString() << endl; } void UPnPMCastSocket::joinUPnPMCastGroup() { int fd = socketDescriptor(); struct ip_mreq mreq; memset(&mreq,0,sizeof(struct ip_mreq)); mreq.imr_multiaddr.s_addr = inet_addr("239.255.255.250"); mreq.imr_interface.s_addr = htonl(INADDR_ANY); #ifndef Q_OS_WIN if (setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(struct ip_mreq)) < 0) #else if (setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char *)&mreq,sizeof(struct ip_mreq)) < 0) #endif { - qDebug() << "Failed to join multicast group 239.255.255.250" << endl; + qDebug() << "Failed to join multicast group 239.255.255.250"; } } void UPnPMCastSocket::leaveUPnPMCastGroup() { int fd = socketDescriptor(); struct ip_mreq mreq; memset(&mreq,0,sizeof(struct ip_mreq)); mreq.imr_multiaddr.s_addr = inet_addr("239.255.255.250"); mreq.imr_interface.s_addr = htonl(INADDR_ANY); #ifndef Q_OS_WIN if (setsockopt(fd,IPPROTO_IP,IP_DROP_MEMBERSHIP,&mreq,sizeof(struct ip_mreq)) < 0) #else if (setsockopt(fd,IPPROTO_IP,IP_DROP_MEMBERSHIP,(char *)&mreq,sizeof(struct ip_mreq)) < 0) #endif { - qDebug() << "Failed to leave multicast group 239.255.255.250" << endl; + qDebug() << "Failed to leave multicast group 239.255.255.250"; } } } } diff --git a/src/upnp/upnprouter.cpp b/src/upnp/upnprouter.cpp index e360ab23..0c807ade 100644 --- a/src/upnp/upnprouter.cpp +++ b/src/upnp/upnprouter.cpp @@ -1,412 +1,412 @@ /* 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 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2005-2007 Joris Guisson Copyright (C) 2009 Michael Kreitzer */ #include "upnprouter.h" #include "upnpdescriptionparser.h" #include "soap.h" #include #include #include #include #include #include #include namespace Konversation { namespace UPnP { UPnPService::UPnPService() { ready = false; } UPnPService::UPnPService(const UPnPService & s) { this->servicetype = s.servicetype; this->controlurl = s.controlurl; this->eventsuburl = s.eventsuburl; this->serviceid = s.serviceid; this->scpdurl = s.scpdurl; ready = false; } void UPnPService::setProperty(const QString & name,const QString & value) { if (name == QLatin1String("serviceType")) servicetype = value; else if (name == QLatin1String("controlURL")) controlurl = value; else if (name == QLatin1String("eventSubURL")) eventsuburl = value; else if (name == QLatin1String("SCPDURL")) scpdurl = value; else if (name == QLatin1String("serviceId")) serviceid = value; } void UPnPService::clear() { servicetype = controlurl = eventsuburl = scpdurl = serviceid = QString(); } UPnPService & UPnPService::operator = (const UPnPService & s) { this->servicetype = s.servicetype; this->controlurl = s.controlurl; this->eventsuburl = s.eventsuburl; this->serviceid = s.serviceid; this->scpdurl = s.scpdurl; return *this; } /////////////////////////////////////// void UPnPDeviceDescription::setProperty(const QString & name,const QString & value) { if (name == QLatin1String("friendlyName")) friendlyName = value; else if (name == QLatin1String("manufacturer")) manufacturer = value; else if (name == QLatin1String("modelDescription")) modelDescription = value; else if (name == QLatin1String("modelName")) modelName = value; else if (name == QLatin1String("modelNumber")) modelNumber = value; } /////////////////////////////////////// UPnPRouter::UPnPRouter(const QString & server,const QUrl &location,const QString & uuid) : server(server),location(location),uuid(uuid) { } UPnPRouter::~UPnPRouter() { QListIterator itr(forwards); while (itr.hasNext()) { Forwarding *check = itr.next(); undoForward(check->port, check->proto); } // We need to give time for the QNetworkManager to process the undo forward commands. Continue // Processing the event loop from here until there are no more forwards. while(forwards.size() > 0) { QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); } } void UPnPRouter::addService(const UPnPService & s) { if (!( s.servicetype.contains(QLatin1String("WANIPConnection")) || s.servicetype.contains(QLatin1String("WANPPPConnection")) )) return; // Confirm this service is connected. Place in pending queue. KJob *req = getStatusInfo(s); if (req) pending_services[req] = s; } void UPnPRouter::downloadFinished(KJob* j) { if (j->error()) { error = i18n("Failed to download %1: %2",location.url(),j->errorString()); - qDebug() << error << endl; + qDebug() << error; return; } KIO::StoredTransferJob* st = qobject_cast(j); // load in the file (target is always local) UPnPDescriptionParser desc_parse; bool ret = desc_parse.parse(st->data(),this); if (!ret) { error = i18n("Error parsing router description."); } emit xmlFileDownloaded(this,ret); } void UPnPRouter::downloadXMLFile() { error.clear(); // downlaod XML description into a temporary file in /tmp - qDebug() << "Downloading XML file " << location << endl; + qDebug() << "Downloading XML file " << location; KIO::Job* job = KIO::storedGet(location,KIO::NoReload, KIO::Overwrite | KIO::HideProgressInfo); connect(job, &KIO::Job::result, this, &UPnPRouter::downloadFinished); } KJob *UPnPRouter::getStatusInfo(const UPnPService &s) { - qDebug() << "UPnP - Checking service status: " << s.servicetype << endl; + qDebug() << "UPnP - Checking service status: " << s.servicetype; QString action = QStringLiteral("GetStatusInfo"); QString comm = SOAP::createCommand(action,s.servicetype); return sendSoapQuery(comm,s.servicetype + '#' + action,s.controlurl); } bool UPnPRouter::forward(const QHostAddress & host, quint16 port, QAbstractSocket::SocketType proto) { - qDebug() << "Forwarding port " << host.toString() << port << " (" << (proto == QAbstractSocket::TcpSocket ? "TCP" : "UDP") << ")" << endl; + qDebug() << "Forwarding port " << host.toString() << port << " (" << (proto == QAbstractSocket::TcpSocket ? "TCP" : "UDP") << ")"; if (service.ready) { // add all the arguments for the command QList args; SOAP::Arg a; a.element = QStringLiteral("NewRemoteHost"); args.append(a); // the external port a.element = QStringLiteral("NewExternalPort"); a.value = QString::number(port); args.append(a); // the protocol a.element = QStringLiteral("NewProtocol"); a.value = proto == QAbstractSocket::TcpSocket ? "TCP" : "UDP"; args.append(a); // the local port a.element = QStringLiteral("NewInternalPort"); a.value = QString::number(port); args.append(a); // the local IP address a.element = QStringLiteral("NewInternalClient"); a.value = host.toString(); args.append(a); a.element = QStringLiteral("NewEnabled"); a.value = '1'; args.append(a); a.element = QStringLiteral("NewPortMappingDescription"); a.value = QStringLiteral("Konversation UPNP"); args.append(a); a.element = QStringLiteral("NewLeaseDuration"); a.value = '0'; args.append(a); QString action = QStringLiteral("AddPortMapping"); QString comm = SOAP::createCommand(action,service.servicetype,args); Forwarding *forward = new Forwarding; forward->port = port; forward->host = host; forward->proto = proto; if (KJob *req = sendSoapQuery(comm,service.servicetype + '#' + action,service.controlurl)) { // erase old forwarding if one exists // The UPnP spec states if an IGD receives a forward request that matches an existing request that it must accept it. QListIterator itr(forwards); while (itr.hasNext()) { Forwarding *check = itr.next(); if (check->port == forward->port && check->host == forward->host && check->proto == forward->proto) { forwards.removeAll(check); delete check; } } forwards.append(forward); pending_forwards[req] = forward; return true; } qDebug() << "Forwarding Failed: Failed to send SOAP query."; delete forward; } qDebug() << "Forwarding Failed: No UPnP Service."; return false; } bool UPnPRouter::undoForward(quint16 port, QAbstractSocket::SocketType proto) { qDebug() << "Undoing forward of port " << port - << " (" << (proto == QAbstractSocket::TcpSocket ? "TCP" : "UDP") << ")" << endl; + << " (" << (proto == QAbstractSocket::TcpSocket ? "TCP" : "UDP") << ")"; if (service.ready) { Forwarding *forward = nullptr; QListIterator itr(forwards); while (itr.hasNext()) { Forwarding *check = itr.next(); if (check->port == port && check->proto == proto) forward = check; } if (forward == nullptr || pending_forwards.keys(forward).size() > 0) return false; // Either forward not found or forward is still pending // add all the arguments for the command QList args; SOAP::Arg a; a.element = QStringLiteral("NewRemoteHost"); args.append(a); // the external port a.element = QStringLiteral("NewExternalPort"); a.value = QString::number(forward->port); args.append(a); // the protocol a.element = QStringLiteral("NewProtocol"); a.value = forward->proto == QAbstractSocket::TcpSocket ? "TCP" : "UDP"; args.append(a); QString action = QStringLiteral("DeletePortMapping"); QString comm = SOAP::createCommand(action,service.servicetype,args); if (KJob *req = sendSoapQuery(comm,service.servicetype + '#' + action,service.controlurl)) { pending_unforwards[req] = forward; return true; } qDebug() << "Undo forwarding Failed: Failed to send SOAP query."; } qDebug() << "Undo forwarding Failed: No UPnP Service."; return false; } KJob *UPnPRouter::sendSoapQuery(const QString & query,const QString & soapact,const QString & controlurl) { // if port is not set, 0 will be returned // thanks to Diego R. Brogna for spotting this bug if (location.port()<=0) location.setPort(80); QUrl address; address.setScheme(QStringLiteral("http")); address.setHost(location.host()); address.setPort(location.port()); address.setPath(controlurl); KIO::TransferJob *req = KIO::http_post( address, query.toLatin1(), KIO::HideProgressInfo ); req->addMetaData(QStringLiteral("content-type"), QStringLiteral("text/xml")); req->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("Konversation UPnP")); req->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("SOAPAction: ") + soapact); soap_data_out[req] = QByteArray(); soap_data_in[req] = QByteArray(); connect(req, &KIO::TransferJob::data, this, &UPnPRouter::recvSoapData); connect(req, &KIO::TransferJob::dataReq, this, &UPnPRouter::sendSoapData); connect(req, &KIO::TransferJob::result, this, &UPnPRouter::onRequestFinished); return req; } void UPnPRouter::sendSoapData(KIO::Job *job, QByteArray &data) { data.append(soap_data_out[job]); soap_data_out[job].clear(); } void UPnPRouter::recvSoapData(KIO::Job *job, const QByteArray &data) { soap_data_in[job].append(data); } void UPnPRouter::onRequestFinished(KJob *r) { if (r->error()) { - qDebug() << "UPnPRouter : Error: " << r->errorString() << endl; + qDebug() << "UPnPRouter : Error: " << r->errorString(); if (pending_services.contains(r)) { pending_services.remove(r); } else if (pending_forwards.contains(r)) { emit forwardComplete(true, pending_forwards[r]->port); forwards.removeAll(pending_forwards[r]); pending_forwards.remove(r); } else if (pending_unforwards.contains(r)) { emit unforwardComplete(true, pending_unforwards[r]->port); forwards.removeAll(pending_unforwards[r]); pending_unforwards.remove(r); } } else { QString reply(soap_data_in[r]); soap_data_in[r].clear(); - qDebug() << "UPnPRouter : OK:" << endl; + qDebug() << "UPnPRouter : OK:"; if (pending_services.contains(r)) { if (reply.contains(QLatin1String("Connected"))) { // Lets just deal with one connected service for now. Last one wins. service = pending_services[r]; service.ready = true; - qDebug() << "Found connected service: " << service.servicetype << endl; + qDebug() << "Found connected service: " << service.servicetype; } pending_services.remove(r); } else if (pending_forwards.contains(r)) { emit forwardComplete(false, pending_forwards[r]->port); pending_forwards.remove(r); } else if (pending_unforwards.contains(r)) { emit unforwardComplete(false, pending_unforwards[r]->port); forwards.removeAll(pending_unforwards[r]); pending_unforwards.remove(r); } } soap_data_in.remove(r); soap_data_out.remove(r); } } }