/* * Copyright (C) 2008-2012 The QXmpp developers * * Author: * Jeremy Lainé * * Source: * http://code.google.com/p/qxmpp * * This file is a part of QXmpp library. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * */ #include #include #include #include #include #include #include #include #include #include #include "QXmppByteStreamIq.h" #include "QXmppClient.h" #include "QXmppConstants.h" #include "QXmppIbbIq.h" #include "QXmppSocks.h" #include "QXmppStreamInitiationIq_p.h" #include "QXmppTransferManager.h" #include "QXmppTransferManager_p.h" #include "QXmppUtils.h" // time to try to connect to a SOCKS host (7 seconds) const int socksTimeout = 7000; static QString streamHash(const QString &sid, const QString &initiatorJid, const QString &targetJid) { QCryptographicHash hash(QCryptographicHash::Sha1); QString str = sid + initiatorJid + targetJid; hash.addData(str.toAscii()); return hash.result().toHex(); } class QXmppTransferFileInfoPrivate : public QSharedData { public: QXmppTransferFileInfoPrivate(); QDateTime date; QByteArray hash; QString name; QString description; qint64 size; }; QXmppTransferFileInfoPrivate::QXmppTransferFileInfoPrivate() : size(0) { } QXmppTransferFileInfo::QXmppTransferFileInfo() : d(new QXmppTransferFileInfoPrivate) { } QXmppTransferFileInfo::QXmppTransferFileInfo(const QXmppTransferFileInfo &other) : d(other.d) { } QXmppTransferFileInfo::~QXmppTransferFileInfo() { } QDateTime QXmppTransferFileInfo::date() const { return d->date; } void QXmppTransferFileInfo::setDate(const QDateTime &date) { d->date = date; } QByteArray QXmppTransferFileInfo::hash() const { return d->hash; } void QXmppTransferFileInfo::setHash(const QByteArray &hash) { d->hash = hash; } QString QXmppTransferFileInfo::name() const { return d->name; } void QXmppTransferFileInfo::setName(const QString &name) { d->name = name; } QString QXmppTransferFileInfo::description() const { return d->description; } void QXmppTransferFileInfo::setDescription(const QString &description) { d->description = description; } qint64 QXmppTransferFileInfo::size() const { return d->size; } void QXmppTransferFileInfo::setSize(qint64 size) { d->size = size; } bool QXmppTransferFileInfo::isNull() const { return d->date.isNull() && d->description.isEmpty() && d->hash.isEmpty() && d->name.isEmpty() && d->size == 0; } QXmppTransferFileInfo& QXmppTransferFileInfo::operator=(const QXmppTransferFileInfo &other) { d = other.d; return *this; } bool QXmppTransferFileInfo::operator==(const QXmppTransferFileInfo &other) const { return other.d->size == d->size && other.d->hash == d->hash && other.d->name == d->name; } void QXmppTransferFileInfo::parse(const QDomElement &element) { d->date = QXmppUtils::datetimeFromString(element.attribute("date")); d->hash = QByteArray::fromHex(element.attribute("hash").toAscii()); d->name = element.attribute("name"); d->size = element.attribute("size").toLongLong(); d->description = element.firstChildElement("desc").text(); } void QXmppTransferFileInfo::toXml(QXmlStreamWriter *writer) const { writer->writeStartElement("file"); writer->writeAttribute("xmlns", ns_stream_initiation_file_transfer); if (d->date.isValid()) writer->writeAttribute("date", QXmppUtils::datetimeToString(d->date)); if (!d->hash.isEmpty()) writer->writeAttribute("hash", d->hash.toHex()); if (!d->name.isEmpty()) writer->writeAttribute("name", d->name); if (d->size > 0) writer->writeAttribute("size", QString::number(d->size)); if (!d->description.isEmpty()) writer->writeTextElement("desc", d->description); writer->writeEndElement(); } class QXmppTransferJobPrivate { public: QXmppTransferJobPrivate(); int blockSize; QXmppClient *client; QXmppTransferJob::Direction direction; qint64 done; QXmppTransferJob::Error error; QCryptographicHash hash; QIODevice *iodevice; QString offerId; QString jid; QUrl localFileUrl; QString sid; QXmppTransferJob::Method method; QString mimeType; QString requestId; QXmppTransferJob::State state; QTime transferStart; // file meta-data QXmppTransferFileInfo fileInfo; // for in-band bytestreams int ibbSequence; // for socks5 bytestreams QTcpSocket *socksSocket; QXmppByteStreamIq::StreamHost socksProxy; }; QXmppTransferJobPrivate::QXmppTransferJobPrivate() : blockSize(16384), client(0), done(0), error(QXmppTransferJob::NoError), hash(QCryptographicHash::Md5), iodevice(0), method(QXmppTransferJob::NoMethod), state(QXmppTransferJob::OfferState), ibbSequence(0), socksSocket(0) { } QXmppTransferJob::QXmppTransferJob(const QString &jid, QXmppTransferJob::Direction direction, QXmppClient *client, QObject *parent) : QXmppLoggable(parent), d(new QXmppTransferJobPrivate) { d->client = client; d->direction = direction; d->jid = jid; } QXmppTransferJob::~QXmppTransferJob() { delete d; } /// Call this method if you wish to abort on ongoing transfer job. /// void QXmppTransferJob::abort() { terminate(AbortError); } /// Call this method if you wish to accept an incoming transfer job. /// void QXmppTransferJob::accept(const QString &filePath) { if (d->direction == IncomingDirection && d->state == OfferState && !d->iodevice) { QFile *file = new QFile(filePath, this); if (!file->open(QIODevice::WriteOnly)) { warning(QString("Could not write to %1").arg(filePath)); abort(); return; } d->iodevice = file; setLocalFileUrl(QUrl::fromLocalFile(filePath)); setState(QXmppTransferJob::StartState); } } /// Call this method if you wish to accept an incoming transfer job. /// void QXmppTransferJob::accept(QIODevice *iodevice) { if (d->direction == IncomingDirection && d->state == OfferState && !d->iodevice) { d->iodevice = iodevice; setState(QXmppTransferJob::StartState); } } /// Returns the job's transfer direction. /// QXmppTransferJob::Direction QXmppTransferJob::direction() const { return d->direction; } /// Returns the last error that was encountered. /// QXmppTransferJob::Error QXmppTransferJob::error() const { return d->error; } /// Returns the remote party's JID. /// QString QXmppTransferJob::jid() const { return d->jid; } /// Returns the local file URL. /// QUrl QXmppTransferJob::localFileUrl() const { return d->localFileUrl; } /// Sets the local file URL. /// /// \note You do not need to call this method if you called accept() /// with a file path. void QXmppTransferJob::setLocalFileUrl(const QUrl &localFileUrl) { if (localFileUrl != d->localFileUrl) { d->localFileUrl = localFileUrl; emit localFileUrlChanged(localFileUrl); } } /// Returns meta-data about the file being transferred. /// QXmppTransferFileInfo QXmppTransferJob::fileInfo() const { return d->fileInfo; } /// \cond QDateTime QXmppTransferJob::fileDate() const { return d->fileInfo.date(); } QByteArray QXmppTransferJob::fileHash() const { return d->fileInfo.hash(); } QString QXmppTransferJob::fileName() const { return d->fileInfo.name(); } qint64 QXmppTransferJob::fileSize() const { return d->fileInfo.size(); } /// \endcond /// Returns the job's transfer method. /// QXmppTransferJob::Method QXmppTransferJob::method() const { return d->method; } /// Returns the job's session identifier. /// QString QXmppTransferJob::sid() const { return d->sid; } /// Returns the job's transfer speed in bytes per second. /// /// If the transfer has not started yet or is already finished, returns 0. /// qint64 QXmppTransferJob::speed() const { qint64 elapsed = d->transferStart.elapsed(); if (d->state != QXmppTransferJob::TransferState || !elapsed) return 0; return (d->done * 1000.0) / elapsed; } /// Returns the job's state. /// QXmppTransferJob::State QXmppTransferJob::state() const { return d->state; } void QXmppTransferJob::setState(QXmppTransferJob::State state) { if (d->state != state) { d->state = state; if (d->state == QXmppTransferJob::TransferState) d->transferStart.start(); emit stateChanged(d->state); } } void QXmppTransferJob::_q_terminated() { emit stateChanged(d->state); if (d->error != NoError) emit error(d->error); emit finished(); } void QXmppTransferJob::terminate(QXmppTransferJob::Error cause) { if (d->state == FinishedState) return; // change state d->error = cause; d->state = FinishedState; // close IO device if (d->iodevice) d->iodevice->close(); // close socket if (d->socksSocket) { d->socksSocket->flush(); d->socksSocket->close(); } // emit signals later QTimer::singleShot(0, this, SLOT(_q_terminated())); } /// \cond QXmppTransferIncomingJob::QXmppTransferIncomingJob(const QString& jid, QXmppClient* client, QObject* parent) : QXmppTransferJob(jid, IncomingDirection, client, parent) , m_candidateClient(0) , m_candidateTimer(0) { } void QXmppTransferIncomingJob::checkData() { if ((d->fileInfo.size() && d->done != d->fileInfo.size()) || (!d->fileInfo.hash().isEmpty() && d->hash.result() != d->fileInfo.hash())) terminate(QXmppTransferJob::FileCorruptError); else terminate(QXmppTransferJob::NoError); } void QXmppTransferIncomingJob::connectToNextHost() { bool check; Q_UNUSED(check); if (m_streamCandidates.isEmpty()) { // could not connect to any stream host QXmppByteStreamIq response; response.setId(m_streamOfferId); response.setTo(m_streamOfferFrom); QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::ItemNotFound); error.setCode(404); response.setType(QXmppIq::Error); response.setError(error); d->client->sendPacket(response); terminate(QXmppTransferJob::ProtocolError); return; } // try next host m_candidateHost = m_streamCandidates.takeFirst(); info(QString("Connecting to streamhost: %1 (%2 %3)").arg( m_candidateHost.jid(), m_candidateHost.host(), QString::number(m_candidateHost.port()))); const QString hostName = streamHash(d->sid, d->jid, d->client->configuration().jid()); // try to connect to stream host m_candidateClient = new QXmppSocksClient(m_candidateHost.host(), m_candidateHost.port(), this); m_candidateTimer = new QTimer(this); check = connect(m_candidateClient, SIGNAL(disconnected()), this, SLOT(_q_candidateDisconnected())); Q_ASSERT(check); check = connect(m_candidateClient, SIGNAL(ready()), this, SLOT(_q_candidateReady())); Q_ASSERT(check); check = connect(m_candidateTimer, SIGNAL(timeout()), this, SLOT(_q_candidateDisconnected())); Q_ASSERT(check); m_candidateTimer->setSingleShot(true); m_candidateTimer->start(socksTimeout); m_candidateClient->connectToHost(hostName, 0); } void QXmppTransferIncomingJob::connectToHosts(const QXmppByteStreamIq &iq) { bool check; Q_UNUSED(check); m_streamCandidates = iq.streamHosts(); m_streamOfferId = iq.id(); m_streamOfferFrom = iq.from(); connectToNextHost(); } bool QXmppTransferIncomingJob::writeData(const QByteArray &data) { const qint64 written = d->iodevice->write(data); if (written < 0) return false; d->done += written; if (!d->fileInfo.hash().isEmpty()) d->hash.addData(data); progress(d->done, d->fileInfo.size()); return true; } void QXmppTransferIncomingJob::_q_candidateReady() { bool check; Q_UNUSED(check); if (!m_candidateClient) return; info(QString("Connected to streamhost: %1 (%2 %3)").arg( m_candidateHost.jid(), m_candidateHost.host(), QString::number(m_candidateHost.port()))); setState(QXmppTransferJob::TransferState); d->socksSocket = m_candidateClient; m_candidateClient = 0; m_candidateTimer->deleteLater(); m_candidateTimer = 0; check = connect(d->socksSocket, SIGNAL(readyRead()), this, SLOT(_q_receiveData())); Q_ASSERT(check); check = connect(d->socksSocket, SIGNAL(disconnected()), this, SLOT(_q_disconnected())); Q_ASSERT(check); QXmppByteStreamIq ackIq; ackIq.setId(m_streamOfferId); ackIq.setTo(m_streamOfferFrom); ackIq.setType(QXmppIq::Result); ackIq.setSid(d->sid); ackIq.setStreamHostUsed(m_candidateHost.jid()); d->client->sendPacket(ackIq); } void QXmppTransferIncomingJob::_q_candidateDisconnected() { if (!m_candidateClient) return; warning(QString("Failed to connect to streamhost: %1 (%2 %3)").arg( m_candidateHost.jid(), m_candidateHost.host(), QString::number(m_candidateHost.port()))); m_candidateClient->deleteLater(); m_candidateClient = 0; m_candidateTimer->deleteLater(); m_candidateTimer = 0; // try next host connectToNextHost(); } void QXmppTransferIncomingJob::_q_disconnected() { if (d->state == QXmppTransferJob::FinishedState) return; checkData(); } void QXmppTransferIncomingJob::_q_receiveData() { if (d->state != QXmppTransferJob::TransferState) return; // receive data block if (d->direction == QXmppTransferJob::IncomingDirection) { writeData(d->socksSocket->readAll()); // if we have received all the data, stop here if (fileSize() && d->done >= fileSize()) checkData(); } } QXmppTransferOutgoingJob::QXmppTransferOutgoingJob(const QString& jid, QXmppClient* client, QObject* parent) : QXmppTransferJob(jid, OutgoingDirection, client, parent) { } void QXmppTransferOutgoingJob::connectToProxy() { bool check; Q_UNUSED(check); info(QString("Connecting to proxy: %1 (%2 %3)").arg( d->socksProxy.jid(), d->socksProxy.host(), QString::number(d->socksProxy.port()))); const QString hostName = streamHash(d->sid, d->client->configuration().jid(), d->jid); QXmppSocksClient *socksClient = new QXmppSocksClient(d->socksProxy.host(), d->socksProxy.port(), this); check = connect(socksClient, SIGNAL(disconnected()), this, SLOT(_q_disconnected())); Q_ASSERT(check); check = connect(socksClient, SIGNAL(ready()), this, SLOT(_q_proxyReady())); Q_ASSERT(check); d->socksSocket = socksClient; socksClient->connectToHost(hostName, 0); } void QXmppTransferOutgoingJob::startSending() { bool check; Q_UNUSED(check); setState(QXmppTransferJob::TransferState); check = connect(d->socksSocket, SIGNAL(bytesWritten(qint64)), this, SLOT(_q_sendData())); Q_ASSERT(check); check = connect(d->iodevice, SIGNAL(readyRead()), this, SLOT(_q_sendData())); Q_ASSERT(check); _q_sendData(); } void QXmppTransferOutgoingJob::_q_disconnected() { if (d->state == QXmppTransferJob::FinishedState) return; if (fileSize() && d->done != fileSize()) terminate(QXmppTransferJob::ProtocolError); else terminate(QXmppTransferJob::NoError); } void QXmppTransferOutgoingJob::_q_proxyReady() { // activate stream QXmppByteStreamIq streamIq; streamIq.setType(QXmppIq::Set); streamIq.setFrom(d->client->configuration().jid()); streamIq.setTo(d->socksProxy.jid()); streamIq.setSid(d->sid); streamIq.setActivate(d->jid); d->requestId = streamIq.id(); d->client->sendPacket(streamIq); } void QXmppTransferOutgoingJob::_q_sendData() { if (d->state != QXmppTransferJob::TransferState) return; // don't saturate the outgoing socket if (d->socksSocket->bytesToWrite() > 2 * d->blockSize) return; // check whether we have written the whole file if (d->fileInfo.size() && d->done >= d->fileInfo.size()) { if (!d->socksSocket->bytesToWrite()) terminate(QXmppTransferJob::NoError); return; } char *buffer = new char[d->blockSize]; qint64 length = d->iodevice->read(buffer, d->blockSize); if (length < 0) { delete [] buffer; terminate(QXmppTransferJob::FileAccessError); return; } if (length > 0) { d->socksSocket->write(buffer, length); delete [] buffer; d->done += length; emit progress(d->done, fileSize()); } } /// \endcond class QXmppTransferManagerPrivate { public: QXmppTransferManagerPrivate(QXmppTransferManager *qq); QXmppTransferIncomingJob *getIncomingJobByRequestId(const QString &jid, const QString &id); QXmppTransferIncomingJob *getIncomingJobBySid(const QString &jid, const QString &sid); QXmppTransferOutgoingJob *getOutgoingJobByRequestId(const QString &jid, const QString &id); int ibbBlockSize; QList jobs; QString proxy; bool proxyOnly; QXmppSocksServer *socksServer; QXmppTransferJob::Methods supportedMethods; private: QXmppTransferJob *getJobByRequestId(QXmppTransferJob::Direction direction, const QString &jid, const QString &id); QXmppTransferManager *q; }; QXmppTransferManagerPrivate::QXmppTransferManagerPrivate(QXmppTransferManager *qq) : ibbBlockSize(4096) , proxyOnly(false) , socksServer(0) , supportedMethods(QXmppTransferJob::AnyMethod) , q(qq) { } QXmppTransferJob* QXmppTransferManagerPrivate::getJobByRequestId(QXmppTransferJob::Direction direction, const QString &jid, const QString &id) { foreach (QXmppTransferJob *job, jobs) if (job->d->direction == direction && job->d->jid == jid && job->d->requestId == id) return job; return 0; } QXmppTransferIncomingJob *QXmppTransferManagerPrivate::getIncomingJobByRequestId(const QString &jid, const QString &id) { return static_cast(getJobByRequestId(QXmppTransferJob::IncomingDirection, jid, id)); } QXmppTransferIncomingJob* QXmppTransferManagerPrivate::getIncomingJobBySid(const QString &jid, const QString &sid) { foreach (QXmppTransferJob *job, jobs) if (job->d->direction == QXmppTransferJob::IncomingDirection && job->d->jid == jid && job->d->sid == sid) return static_cast(job); return 0; } QXmppTransferOutgoingJob *QXmppTransferManagerPrivate::getOutgoingJobByRequestId(const QString &jid, const QString &id) { return static_cast(getJobByRequestId(QXmppTransferJob::OutgoingDirection, jid, id)); } /// Constructs a QXmppTransferManager to handle incoming and outgoing /// file transfers. QXmppTransferManager::QXmppTransferManager() { bool check; Q_UNUSED(check); d = new QXmppTransferManagerPrivate(this); // start SOCKS server d->socksServer = new QXmppSocksServer(this); if (d->socksServer->listen()) { check = connect(d->socksServer, SIGNAL(newConnection(QTcpSocket*,QString,quint16)), this, SLOT(_q_socksServerConnected(QTcpSocket*,QString,quint16))); Q_ASSERT(check); } else { qWarning("QXmppSocksServer could not start listening"); } } QXmppTransferManager::~QXmppTransferManager() { delete d; } void QXmppTransferManager::byteStreamIqReceived(const QXmppByteStreamIq &iq) { // handle IQ from proxy foreach (QXmppTransferJob *job, d->jobs) { if (job->d->socksProxy.jid() == iq.from() && job->d->requestId == iq.id()) { if (iq.type() == QXmppIq::Result && iq.streamHosts().size() > 0) { job->d->socksProxy = iq.streamHosts().first(); socksServerSendOffer(job); return; } } } if (iq.type() == QXmppIq::Result) byteStreamResultReceived(iq); else if (iq.type() == QXmppIq::Set) byteStreamSetReceived(iq); } /// Handle a response to a bystream set, i.e. after we informed the remote party /// that we connected to a stream host. void QXmppTransferManager::byteStreamResponseReceived(const QXmppIq &iq) { QXmppTransferJob *job = d->getIncomingJobByRequestId(iq.from(), iq.id()); if (!job || job->method() != QXmppTransferJob::SocksMethod || job->state() != QXmppTransferJob::StartState) return; if (iq.type() == QXmppIq::Error) job->terminate(QXmppTransferJob::ProtocolError); } /// Handle a bytestream result, i.e. after the remote party has connected to /// a stream host. void QXmppTransferManager::byteStreamResultReceived(const QXmppByteStreamIq &iq) { bool check; Q_UNUSED(check); QXmppTransferOutgoingJob *job = d->getOutgoingJobByRequestId(iq.from(), iq.id()); if (!job || job->method() != QXmppTransferJob::SocksMethod || job->state() != QXmppTransferJob::StartState) return; // check the stream host if (iq.streamHostUsed() == job->d->socksProxy.jid()) { job->connectToProxy(); return; } // direction connection, start sending data if (!job->d->socksSocket) { warning("Client says they connected to our SOCKS server, but they did not"); job->terminate(QXmppTransferJob::ProtocolError); return; } check = connect(job->d->socksSocket, SIGNAL(disconnected()), job, SLOT(_q_disconnected())); Q_ASSERT(check); job->startSending(); } /// Handle a bytestream set, i.e. an invitation from the remote party to connect /// to a stream host. void QXmppTransferManager::byteStreamSetReceived(const QXmppByteStreamIq &iq) { bool check; Q_UNUSED(check); QXmppIq response; response.setId(iq.id()); response.setTo(iq.from()); QXmppTransferIncomingJob *job = d->getIncomingJobBySid(iq.from(), iq.sid()); if (!job || job->method() != QXmppTransferJob::SocksMethod || job->state() != QXmppTransferJob::StartState) { // the stream is unknown QXmppStanza::Error error(QXmppStanza::Error::Auth, QXmppStanza::Error::NotAcceptable); error.setCode(406); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } job->connectToHosts(iq); } /// \cond QStringList QXmppTransferManager::discoveryFeatures() const { return QStringList() << ns_ibb // XEP-0047: In-Band Bytestreams << ns_bytestreams // XEP-0065: SOCKS5 Bytestreams << ns_stream_initiation // XEP-0095: Stream Initiation << ns_stream_initiation_file_transfer; // XEP-0096: SI File Transfer } bool QXmppTransferManager::handleStanza(const QDomElement &element) { if (element.tagName() != "iq") return false; // XEP-0047 In-Band Bytestreams if(QXmppIbbCloseIq::isIbbCloseIq(element)) { QXmppIbbCloseIq ibbCloseIq; ibbCloseIq.parse(element); ibbCloseIqReceived(ibbCloseIq); return true; } else if(QXmppIbbDataIq::isIbbDataIq(element)) { QXmppIbbDataIq ibbDataIq; ibbDataIq.parse(element); ibbDataIqReceived(ibbDataIq); return true; } else if(QXmppIbbOpenIq::isIbbOpenIq(element)) { QXmppIbbOpenIq ibbOpenIq; ibbOpenIq.parse(element); ibbOpenIqReceived(ibbOpenIq); return true; } // XEP-0065: SOCKS5 Bytestreams else if(QXmppByteStreamIq::isByteStreamIq(element)) { QXmppByteStreamIq byteStreamIq; byteStreamIq.parse(element); byteStreamIqReceived(byteStreamIq); return true; } // XEP-0095: Stream Initiation else if(QXmppStreamInitiationIq::isStreamInitiationIq(element)) { QXmppStreamInitiationIq siIq; siIq.parse(element); streamInitiationIqReceived(siIq); return true; } return false; } void QXmppTransferManager::setClient(QXmppClient *client) { bool check; Q_UNUSED(check); QXmppClientExtension::setClient(client); // XEP-0047: In-Band Bytestreams check = connect(client, SIGNAL(iqReceived(QXmppIq)), this, SLOT(_q_iqReceived(QXmppIq))); Q_ASSERT(check); } /// \endcond void QXmppTransferManager::ibbCloseIqReceived(const QXmppIbbCloseIq &iq) { QXmppIq response; response.setTo(iq.from()); response.setId(iq.id()); QXmppTransferIncomingJob *job = d->getIncomingJobBySid(iq.from(), iq.sid()); if (!job || job->method() != QXmppTransferJob::InBandMethod) { // the job is unknown, cancel it QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::ItemNotFound); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } // acknowledge the packet response.setType(QXmppIq::Result); client()->sendPacket(response); // check received data job->checkData(); } void QXmppTransferManager::ibbDataIqReceived(const QXmppIbbDataIq &iq) { QXmppIq response; response.setTo(iq.from()); response.setId(iq.id()); QXmppTransferIncomingJob *job = d->getIncomingJobBySid(iq.from(), iq.sid()); if (!job || job->method() != QXmppTransferJob::InBandMethod || job->state() != QXmppTransferJob::TransferState) { // the job is unknown, cancel it QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::ItemNotFound); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } if (iq.sequence() != job->d->ibbSequence) { // the packet is out of sequence QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::UnexpectedRequest); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } // write data job->writeData(iq.payload()); job->d->ibbSequence++; // acknowledge the packet response.setType(QXmppIq::Result); client()->sendPacket(response); } void QXmppTransferManager::ibbOpenIqReceived(const QXmppIbbOpenIq &iq) { QXmppIq response; response.setTo(iq.from()); response.setId(iq.id()); QXmppTransferJob *job = d->getIncomingJobBySid(iq.from(), iq.sid()); if (!job || job->method() != QXmppTransferJob::InBandMethod) { // the job is unknown, cancel it QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::ItemNotFound); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } if (iq.blockSize() > d->ibbBlockSize) { // we prefer a smaller block size QXmppStanza::Error error(QXmppStanza::Error::Modify, QXmppStanza::Error::ResourceConstraint); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } job->d->blockSize = iq.blockSize(); job->setState(QXmppTransferJob::TransferState); // accept transfer response.setType(QXmppIq::Result); client()->sendPacket(response); } void QXmppTransferManager::ibbResponseReceived(const QXmppIq &iq) { QXmppTransferJob *job = d->getOutgoingJobByRequestId(iq.from(), iq.id()); if (!job || job->method() != QXmppTransferJob::InBandMethod || job->state() == QXmppTransferJob::FinishedState) return; // if the IO device is closed, do nothing if (!job->d->iodevice->isOpen()) return; if (iq.type() == QXmppIq::Result) { const QByteArray buffer = job->d->iodevice->read(job->d->blockSize); job->setState(QXmppTransferJob::TransferState); if (buffer.size()) { // send next data block QXmppIbbDataIq dataIq; dataIq.setTo(job->d->jid); dataIq.setSid(job->d->sid); dataIq.setSequence(job->d->ibbSequence++); dataIq.setPayload(buffer); job->d->requestId = dataIq.id(); client()->sendPacket(dataIq); job->d->done += buffer.size(); job->progress(job->d->done, job->fileSize()); } else { // close the bytestream QXmppIbbCloseIq closeIq; closeIq.setTo(job->d->jid); closeIq.setSid(job->d->sid); job->d->requestId = closeIq.id(); client()->sendPacket(closeIq); job->terminate(QXmppTransferJob::NoError); } } else if (iq.type() == QXmppIq::Error) { // close the bytestream QXmppIbbCloseIq closeIq; closeIq.setTo(job->d->jid); closeIq.setSid(job->d->sid); job->d->requestId = closeIq.id(); client()->sendPacket(closeIq); job->terminate(QXmppTransferJob::ProtocolError); } } void QXmppTransferManager::_q_iqReceived(const QXmppIq &iq) { bool check; Q_UNUSED(check); foreach (QXmppTransferJob *ptr, d->jobs) { // handle IQ from proxy if (ptr->direction() == QXmppTransferJob::OutgoingDirection && ptr->d->socksProxy.jid() == iq.from() && ptr->d->requestId == iq.id()) { QXmppTransferOutgoingJob *job = static_cast(ptr); if (job->d->socksSocket) { // proxy connection activation result if (iq.type() == QXmppIq::Result) { // proxy stream activated, start sending data job->startSending(); } else if (iq.type() == QXmppIq::Error) { // proxy stream not activated, terminate warning("Could not activate SOCKS5 proxy bytestream"); job->terminate(QXmppTransferJob::ProtocolError); } } else { // we could not get host/port from proxy, procede without a proxy if (iq.type() == QXmppIq::Error) socksServerSendOffer(job); } return; } // handle IQ from peer else if (ptr->d->jid == iq.from() && ptr->d->requestId == iq.id()) { QXmppTransferJob *job = ptr; if (job->direction() == QXmppTransferJob::OutgoingDirection && job->method() == QXmppTransferJob::InBandMethod) { ibbResponseReceived(iq); return; } else if (job->direction() == QXmppTransferJob::IncomingDirection && job->method() == QXmppTransferJob::SocksMethod) { byteStreamResponseReceived(iq); return; } else if (job->direction() == QXmppTransferJob::OutgoingDirection && iq.type() == QXmppIq::Error) { // remote party cancelled stream initiation job->terminate(QXmppTransferJob::AbortError); return; } } } } void QXmppTransferManager::_q_jobDestroyed(QObject *object) { d->jobs.removeAll(static_cast(object)); } void QXmppTransferManager::_q_jobError(QXmppTransferJob::Error error) { QXmppTransferJob *job = qobject_cast(sender()); if (!job || !d->jobs.contains(job)) return; if (job->direction() == QXmppTransferJob::OutgoingDirection && job->method() == QXmppTransferJob::InBandMethod && error == QXmppTransferJob::AbortError) { // close the bytestream QXmppIbbCloseIq closeIq; closeIq.setTo(job->d->jid); closeIq.setSid(job->d->sid); job->d->requestId = closeIq.id(); client()->sendPacket(closeIq); } } void QXmppTransferManager::_q_jobFinished() { QXmppTransferJob *job = qobject_cast(sender()); if (!job || !d->jobs.contains(job)) return; emit jobFinished(job); } void QXmppTransferManager::_q_jobStateChanged(QXmppTransferJob::State state) { bool check; Q_UNUSED(check); QXmppTransferJob *job = qobject_cast(sender()); if (!job || !d->jobs.contains(job)) return; if (job->direction() != QXmppTransferJob::IncomingDirection) return; // disconnect from the signal disconnect(job, SIGNAL(stateChanged(QXmppTransferJob::State)), this, SLOT(_q_jobStateChanged(QXmppTransferJob::State))); // the job was refused by the local party if (state != QXmppTransferJob::StartState || !job->d->iodevice || !job->d->iodevice->isWritable()) { QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::Forbidden); error.setCode(403); QXmppIq response; response.setTo(job->jid()); response.setId(job->d->offerId); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); job->terminate(QXmppTransferJob::AbortError); return; } // the job was accepted by the local party check = connect(job, SIGNAL(error(QXmppTransferJob::Error)), this, SLOT(_q_jobError(QXmppTransferJob::Error))); Q_ASSERT(check); QXmppDataForm form; form.setType(QXmppDataForm::Submit); QXmppDataForm::Field methodField(QXmppDataForm::Field::ListSingleField); methodField.setKey("stream-method"); if (job->method() == QXmppTransferJob::InBandMethod) methodField.setValue(ns_ibb); else if (job->method() == QXmppTransferJob::SocksMethod) methodField.setValue(ns_bytestreams); form.setFields(QList() << methodField); QXmppStreamInitiationIq response; response.setTo(job->jid()); response.setId(job->d->offerId); response.setType(QXmppIq::Result); response.setProfile(QXmppStreamInitiationIq::FileTransfer); response.setFeatureForm(form); client()->sendPacket(response); // notify user emit jobStarted(job); } /// Send file to a remote party. /// /// The remote party will be given the choice to accept or refuse the transfer. /// QXmppTransferJob *QXmppTransferManager::sendFile(const QString &jid, const QString &filePath, const QString &description) { if (jid.isEmpty()) { warning("Refusing to send file to an empty jid"); return 0; } QFileInfo info(filePath); QXmppTransferFileInfo fileInfo; fileInfo.setDate(info.lastModified()); fileInfo.setName(info.fileName()); fileInfo.setSize(info.size()); fileInfo.setDescription(description); // open file QIODevice *device = new QFile(filePath); if (!device->open(QIODevice::ReadOnly)) { warning(QString("Could not read from %1").arg(filePath)); delete device; device = 0; } // hash file if (device && !device->isSequential()) { QCryptographicHash hash(QCryptographicHash::Md5); QByteArray buffer; while (device->bytesAvailable()) { buffer = device->read(16384); hash.addData(buffer); } device->reset(); fileInfo.setHash(hash.result()); } // create job QXmppTransferJob *job = sendFile(jid, device, fileInfo); job->setLocalFileUrl(filePath); return job; } /// Send file to a remote party. /// /// The remote party will be given the choice to accept or refuse the transfer. /// QXmppTransferJob *QXmppTransferManager::sendFile(const QString &jid, QIODevice *device, const QXmppTransferFileInfo &fileInfo, const QString &sid) { bool check; Q_UNUSED(check); if (jid.isEmpty()) { warning("Refusing to send file to an empty jid"); return 0; } QXmppTransferOutgoingJob *job = new QXmppTransferOutgoingJob(jid, client(), this); if (sid.isEmpty()) job->d->sid = QXmppUtils::generateStanzaHash(); else job->d->sid = sid; job->d->fileInfo = fileInfo; job->d->iodevice = device; if (device) device->setParent(job); // check file is open if (!device || !device->isReadable()) { job->terminate(QXmppTransferJob::FileAccessError); return job; } // check we support some methods if (!d->supportedMethods) { job->terminate(QXmppTransferJob::ProtocolError); return job; } // collect supported stream methods QXmppDataForm form; form.setType(QXmppDataForm::Form); QXmppDataForm::Field methodField(QXmppDataForm::Field::ListSingleField); methodField.setKey("stream-method"); if (d->supportedMethods & QXmppTransferJob::InBandMethod) methodField.setOptions(methodField.options() << qMakePair(QString(), QString::fromLatin1(ns_ibb))); if (d->supportedMethods & QXmppTransferJob::SocksMethod) methodField.setOptions(methodField.options() << qMakePair(QString(), QString::fromLatin1(ns_bytestreams))); form.setFields(QList() << methodField); // start job d->jobs.append(job); check = connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(_q_jobDestroyed(QObject*))); Q_ASSERT(check); check = connect(job, SIGNAL(error(QXmppTransferJob::Error)), this, SLOT(_q_jobError(QXmppTransferJob::Error))); Q_ASSERT(check); check = connect(job, SIGNAL(finished()), this, SLOT(_q_jobFinished())); Q_ASSERT(check); QXmppStreamInitiationIq request; request.setType(QXmppIq::Set); request.setTo(jid); request.setProfile(QXmppStreamInitiationIq::FileTransfer); request.setFileInfo(job->d->fileInfo); request.setFeatureForm(form); request.setSiId(job->d->sid); job->d->requestId = request.id(); client()->sendPacket(request); // notify user emit jobStarted(job); return job; } void QXmppTransferManager::_q_socksServerConnected(QTcpSocket *socket, const QString &hostName, quint16 port) { const QString ownJid = client()->configuration().jid(); foreach (QXmppTransferJob *job, d->jobs) { if (hostName == streamHash(job->d->sid, ownJid, job->jid()) && port == 0) { job->d->socksSocket = socket; return; } } warning("QXmppSocksServer got a connection for a unknown stream"); socket->close(); } void QXmppTransferManager::socksServerSendOffer(QXmppTransferJob *job) { const QString ownJid = client()->configuration().jid(); QList streamHosts; // discover local IPs if (!d->proxyOnly) { foreach (const QNetworkInterface &interface, QNetworkInterface::allInterfaces()) { if (!(interface.flags() & QNetworkInterface::IsRunning) || interface.flags() & QNetworkInterface::IsLoopBack) continue; foreach (const QNetworkAddressEntry &entry, interface.addressEntries()) { if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol || entry.netmask().isNull()) continue; QXmppByteStreamIq::StreamHost streamHost; streamHost.setHost(entry.ip().toString()); streamHost.setPort(d->socksServer->serverPort()); streamHost.setJid(ownJid); streamHosts.append(streamHost); } } } // add proxy if (!job->d->socksProxy.jid().isEmpty()) streamHosts.append(job->d->socksProxy); // check we have some stream hosts if (!streamHosts.size()) { warning("Could not determine local stream hosts"); job->terminate(QXmppTransferJob::ProtocolError); return; } // send offer QXmppByteStreamIq streamIq; streamIq.setType(QXmppIq::Set); streamIq.setTo(job->d->jid); streamIq.setSid(job->d->sid); streamIq.setStreamHosts(streamHosts); job->d->requestId = streamIq.id(); client()->sendPacket(streamIq); } void QXmppTransferManager::streamInitiationIqReceived(const QXmppStreamInitiationIq &iq) { if (iq.type() == QXmppIq::Result) streamInitiationResultReceived(iq); else if (iq.type() == QXmppIq::Set) streamInitiationSetReceived(iq); } // The remote party has accepted an outgoing transfer. void QXmppTransferManager::streamInitiationResultReceived(const QXmppStreamInitiationIq &iq) { QXmppTransferJob *job = d->getOutgoingJobByRequestId(iq.from(), iq.id()); if (!job || job->state() != QXmppTransferJob::OfferState) return; foreach (const QXmppDataForm::Field &field, iq.featureForm().fields()) { if (field.key() == "stream-method") { if ((field.value().toString() == ns_ibb) && (d->supportedMethods & QXmppTransferJob::InBandMethod)) job->d->method = QXmppTransferJob::InBandMethod; else if ((field.value().toString() == ns_bytestreams) && (d->supportedMethods & QXmppTransferJob::SocksMethod)) job->d->method = QXmppTransferJob::SocksMethod; } } // remote party accepted stream initiation job->setState(QXmppTransferJob::StartState); if (job->method() == QXmppTransferJob::InBandMethod) { // lower block size for IBB job->d->blockSize = d->ibbBlockSize; QXmppIbbOpenIq openIq; openIq.setTo(job->d->jid); openIq.setSid(job->d->sid); openIq.setBlockSize(job->d->blockSize); job->d->requestId = openIq.id(); client()->sendPacket(openIq); } else if (job->method() == QXmppTransferJob::SocksMethod) { if (!d->socksServer->isListening()) { warning("QXmppSocksServer is not listening"); job->terminate(QXmppTransferJob::ProtocolError); return; } if (!d->proxy.isEmpty()) { job->d->socksProxy.setJid(d->proxy); // query proxy QXmppByteStreamIq streamIq; streamIq.setType(QXmppIq::Get); streamIq.setTo(job->d->socksProxy.jid()); streamIq.setSid(job->d->sid); job->d->requestId = streamIq.id(); client()->sendPacket(streamIq); } else { socksServerSendOffer(job); } } else { warning("QXmppTransferManager received an unsupported method"); job->terminate(QXmppTransferJob::ProtocolError); } } void QXmppTransferManager::streamInitiationSetReceived(const QXmppStreamInitiationIq &iq) { bool check; Q_UNUSED(check); QXmppIq response; response.setTo(iq.from()); response.setId(iq.id()); // check we support the profile if (iq.profile() != QXmppStreamInitiationIq::FileTransfer) { // FIXME : we should add: // QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::BadRequest); error.setCode(400); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } // check there is a receiver connected to the fileReceived() signal if (!receivers(SIGNAL(fileReceived(QXmppTransferJob*)))) { QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::Forbidden); error.setCode(403); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); return; } // check the stream type QXmppTransferIncomingJob *job = new QXmppTransferIncomingJob(iq.from(), client(), this); int offeredMethods = QXmppTransferJob::NoMethod; job->d->offerId = iq.id(); job->d->sid = iq.siId(); job->d->mimeType = iq.mimeType(); job->d->fileInfo = iq.fileInfo(); foreach (const QXmppDataForm::Field &field, iq.featureForm().fields()) { if (field.key() == "stream-method") { QPair option; foreach (option, field.options()) { if (option.second == ns_ibb) offeredMethods = offeredMethods | QXmppTransferJob::InBandMethod; else if (option.second == ns_bytestreams) offeredMethods = offeredMethods | QXmppTransferJob::SocksMethod; } } } // select a method supported by both parties int sharedMethods = (offeredMethods & d->supportedMethods); if (sharedMethods & QXmppTransferJob::SocksMethod) job->d->method = QXmppTransferJob::SocksMethod; else if (sharedMethods & QXmppTransferJob::InBandMethod) job->d->method = QXmppTransferJob::InBandMethod; else { // FIXME : we should add: // QXmppStanza::Error error(QXmppStanza::Error::Cancel, QXmppStanza::Error::BadRequest); error.setCode(400); response.setType(QXmppIq::Error); response.setError(error); client()->sendPacket(response); delete job; return; } // register job d->jobs.append(job); check = connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(_q_jobDestroyed(QObject*))); Q_ASSERT(check); check = connect(job, SIGNAL(finished()), this, SLOT(_q_jobFinished())); Q_ASSERT(check); check = connect(job, SIGNAL(stateChanged(QXmppTransferJob::State)), this, SLOT(_q_jobStateChanged(QXmppTransferJob::State))); Q_ASSERT(check); // allow user to accept or decline the job emit fileReceived(job); } /// Return the JID of the bytestream proxy to use for /// outgoing transfers. /// QString QXmppTransferManager::proxy() const { return d->proxy; } /// Set the JID of the SOCKS5 bytestream proxy to use for /// outgoing transfers. /// /// If you set a proxy, when you send a file the proxy will /// be offered to the recipient in addition to your own IP /// addresses. /// void QXmppTransferManager::setProxy(const QString &proxyJid) { d->proxy = proxyJid; } /// Return whether the proxy will systematically be used for /// outgoing SOCKS5 bytestream transfers. /// bool QXmppTransferManager::proxyOnly() const { return d->proxyOnly; } /// Set whether the proxy should systematically be used for /// outgoing SOCKS5 bytestream transfers. /// /// \note If you set this to true and do not provide a proxy /// using setProxy(), your outgoing transfers will fail! /// void QXmppTransferManager::setProxyOnly(bool proxyOnly) { d->proxyOnly = proxyOnly; } /// Return the supported stream methods. /// /// The methods are a combination of zero or more QXmppTransferJob::Method. /// QXmppTransferJob::Methods QXmppTransferManager::supportedMethods() const { return d->supportedMethods; } /// Set the supported stream methods. This allows you to selectively /// enable or disable stream methods (In-Band or SOCKS5 bytestreams). /// /// The methods argument is a combination of zero or more /// QXmppTransferJob::Method. /// void QXmppTransferManager::setSupportedMethods(QXmppTransferJob::Methods methods) { d->supportedMethods = methods; }