Commit 4ef82924 authored by Linus Jahn's avatar Linus Jahn

Switch to upstream HTTP File Upload impl

Only the QXmppUploadManager hasn't been upstreamed yet. (And it needs
some good refactoring before it can be upstreamed)
parent 3cf8e007
Pipeline #29181 passed with stages
in 23 minutes and 45 seconds
......@@ -54,8 +54,6 @@ set(KAIDAN_SOURCES
src/GuiStyle.h
# kaidan QXmpp extensions (need to be merged into QXmpp upstream)
src/qxmpp-exts/QXmppHttpUploadIq.cpp
src/qxmpp-exts/QXmppUploadRequestManager.cpp
src/qxmpp-exts/QXmppUploadManager.cpp
src/qxmpp-exts/QXmppColorGenerator.cpp
src/qxmpp-exts/QXmppUri.cpp
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QDomElement>
#include <QMimeDatabase>
#include "QXmppHttpUploadIq.h"
bool operator==(const QXmppHttpUploadRequestIq &l, const QXmppHttpUploadRequestIq &r)
{
return l.fileName() == r.fileName() &&
l.size() == r.size() &&
l.contentType() == r.contentType();
}
QString QXmppHttpUploadRequestIq::fileName() const
{
return m_fileName;
}
void QXmppHttpUploadRequestIq::setFileName(const QString &fileName)
{
m_fileName = fileName;
}
qint64 QXmppHttpUploadRequestIq::size() const
{
return m_size;
}
void QXmppHttpUploadRequestIq::setSize(const qint64 &size)
{
m_size = size;
}
QMimeType QXmppHttpUploadRequestIq::contentType() const
{
return m_contentType;
}
void QXmppHttpUploadRequestIq::setContentType(const QMimeType &type)
{
m_contentType = type;
}
/// \cond
bool QXmppHttpUploadRequestIq::isHttpUploadRequestIq(const QDomElement &element)
{
if (element.tagName() == "iq") {
QDomElement request = element.firstChildElement("request");
if (!request.isNull() && request.namespaceURI() == ns_httpupload)
return true;
}
return false;
}
void QXmppHttpUploadRequestIq::parseElementFromChild(const QDomElement &element)
{
QDomElement request = element.firstChildElement("request");
m_fileName = element.attribute("filename");
m_size = element.attribute("size").toLongLong();
if (element.hasAttribute("content-type")) {
QMimeDatabase mimeDb;
QMimeType type = mimeDb.mimeTypeForName(element.attribute("content-type"));
if (!type.isDefault() && type.isValid())
m_contentType = type;
}
}
void QXmppHttpUploadRequestIq::toXmlElementFromChild(QXmlStreamWriter *writer) const
{
writer->writeStartElement("request");
writer->writeAttribute("xmlns", ns_httpupload);
// filename and size are required
writer->writeAttribute("filename", m_fileName);
writer->writeAttribute("size", QString::number(m_size));
// content-type is optional
if (!m_contentType.isDefault() && m_contentType.isValid())
writer->writeAttribute("content-type", m_contentType.name());
writer->writeEndElement();
}
/// \endcond
bool operator==(const QXmppHttpUploadSlotIq &l, const QXmppHttpUploadSlotIq &r)
{
return l.getUrl() == r.getUrl() &&
l.putUrl() == r.putUrl() &&
l.headerFields() == r.headerFields();
}
QUrl QXmppHttpUploadSlotIq::putUrl() const
{
return m_putUrl;
}
void QXmppHttpUploadSlotIq::setPutUrl(const QUrl &putUrl)
{
m_putUrl = putUrl;
}
QUrl QXmppHttpUploadSlotIq::getUrl() const
{
return m_getUrl;
}
void QXmppHttpUploadSlotIq::setGetUrl(const QUrl &getUrl)
{
m_getUrl = getUrl;
}
QMap<QString, QString> QXmppHttpUploadSlotIq::headerFields() const
{
return m_headerFields;
}
void QXmppHttpUploadSlotIq::setHeaderFields(const QMap<QString, QString> &headerFields)
{
m_headerFields.clear();
for (QString &name : headerFields.keys()) {
if (name == "Authorization" || name == "Cookie" || name == "Expires") {
QString value = headerFields[name];
m_headerFields[name] = value.replace("\n", "");
}
}
}
/// Returns whether the URLs (put and get) are both HTTPS URLs. It will return false, if the URLs
/// are only HTTP URLs or they're not set. HTTP URLs are not XEP-compliant.
bool QXmppHttpUploadSlotIq::hasHttpsUrls() const
{
if (m_getUrl.toString().startsWith("https://") &&
m_putUrl.toString().startsWith("https://"))
return true;
return false;
}
/// \cond
bool QXmppHttpUploadSlotIq::isHttpUploadSlotIq(const QDomElement &element)
{
if (element.tagName() == "iq") {
QDomElement slot = element.firstChildElement("slot");
if (!slot.isNull() && slot.namespaceURI() == ns_httpupload)
return true;
}
return false;
}
void QXmppHttpUploadSlotIq::parseElementFromChild(const QDomElement &element)
{
QDomElement slot = element.firstChildElement("slot");
QDomElement put = slot.firstChildElement("put");
m_getUrl = QUrl::fromEncoded(slot.firstChildElement("get").attribute("url").toUtf8());
m_putUrl = QUrl::fromEncoded(put.attribute("url").toUtf8());
if (put.hasChildNodes()) {
QMap<QString, QString> headers;
QDomNodeList headerNodes = put.childNodes();
for (int i = 0; i < headerNodes.length(); i++) {
QDomElement tag = headerNodes.at(i).toElement();
if (tag.tagName() == "header" && tag.hasAttribute("name"))
headers[tag.attribute("name")] = headerNodes.at(i).toCDATASection().data();
}
setHeaderFields(headers);
}
}
void QXmppHttpUploadSlotIq::toXmlElementFromChild(QXmlStreamWriter *writer) const
{
writer->writeStartElement("slot");
writer->writeAttribute("xmlns", ns_httpupload);
writer->writeStartElement("put");
writer->writeAttribute("url", m_putUrl.toEncoded());
if (!m_headerFields.isEmpty()) {
for (const QString &name : m_headerFields.keys()) {
writer->writeStartElement("header");
writer->writeAttribute("name", name);
writer->writeCDATA(m_headerFields[name]);
writer->writeEndElement();
}
}
writer->writeEndElement();
writer->writeStartElement("get");
writer->writeAttribute("url", m_getUrl.toEncoded());
writer->writeEndElement();
writer->writeEndElement();
}
/// \endcond
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QXMPPHTTPUPLOADIQ_H
#define QXMPPHTTPUPLOADIQ_H
#include <QMap>
#include <QMimeType>
#include <QUrl>
#include <QXmppIq.h>
const QString ns_httpupload = "urn:xmpp:http:upload:0";
/// \brief Represents a request to the server for receiving an upload slot.
///
/// XEP-Version: 0.7
///
/// \ingroup Stanzas
class QXmppHttpUploadRequestIq : public QXmppIq
{
public:
QString fileName() const;
void setFileName(const QString &filename);
qint64 size() const;
void setSize(const qint64 &size);
QMimeType contentType() const;
void setContentType(const QMimeType &type);
/// \cond
static bool isHttpUploadRequestIq(const QDomElement &element);
protected:
void parseElementFromChild(const QDomElement &element) override;
void toXmlElementFromChild(QXmlStreamWriter *writer) const override;
/// \endcond
private:
QString m_fileName;
qint64 m_size;
QMimeType m_contentType;
};
bool operator==(const QXmppHttpUploadRequestIq &l, const QXmppHttpUploadRequestIq &r);
/// \brief Represents a slot for uploading a file to using HTTP File Upload. The server will return
/// it after receiving a valid \see QXmppHttpUploadRequestIq.
///
/// XEP-Version: 0.7
///
/// \ingroup Stanzas
class QXmppHttpUploadSlotIq : public QXmppIq
{
public:
QUrl putUrl() const;
void setPutUrl(const QUrl &putUrl);
QUrl getUrl() const;
void setGetUrl(const QUrl &getUrl);
QMap<QString, QString> headerFields() const;
void setHeaderFields(const QMap<QString, QString> &headerFields);
bool hasHttpsUrls() const;
/// \cond
static bool isHttpUploadSlotIq(const QDomElement &element);
protected:
void parseElementFromChild(const QDomElement &element) override;
void toXmlElementFromChild(QXmlStreamWriter *writer) const override;
/// \endcond
private:
QUrl m_putUrl;
QUrl m_getUrl;
QMap<QString, QString> m_headerFields;
};
bool operator==(const QXmppHttpUploadSlotIq &l, const QXmppHttpUploadSlotIq &r);
#endif // QXMPPHTTPUPLOADIQ_H
......@@ -63,7 +63,6 @@ QXmppHttpUpload::QXmppHttpUpload(const QXmppHttpUpload &upload)
QXmppHttpUpload::~QXmppHttpUpload()
{
delete m_netManager;
}
QXmppHttpUpload& QXmppHttpUpload::operator=(const QXmppHttpUpload &other)
......@@ -84,12 +83,8 @@ QXmppHttpUpload& QXmppHttpUpload::operator=(const QXmppHttpUpload &other)
bool operator==(const QXmppHttpUpload &l, const QXmppHttpUpload &r)
{
return l.id() == r.id() &&
l.customFileName() == r.customFileName() &&
l.fileInfo() == r.fileInfo() &&
l.requestId() == r.requestId() &&
l.requestError() == r.requestError() &&
l.slot() == r.slot();
return l.id() == r.id() && l.customFileName() == r.customFileName() &&
l.fileInfo() == r.fileInfo() && l.requestId() == r.requestId();
}
int QXmppHttpUpload::id() const
......@@ -196,9 +191,9 @@ void QXmppHttpUpload::startUpload()
request.setHeader(QNetworkRequest::ContentTypeHeader, mimeType.name());
// other header fields
const QMap<QString, QString> &headers = m_slot.headerFields();
const QStringList headerKeys = headers.keys();
for (const QString &name : headerKeys)
const auto headers = m_slot.putHeaders();
const auto headerKeys = headers.keys();
for (const auto &name : headerKeys)
request.setRawHeader(name.toUtf8(), headers.value(name).toUtf8());
// open file
......@@ -290,14 +285,16 @@ void QXmppUploadManager::startNextUpload()
QXmppHttpUpload *upload = m_runningJobs > 0 ? m_uploads.last() : m_uploads.first();
m_runningJobs++;
QString reqId = requestUploadSlot(upload->fileInfo(), upload->customFileName());
const auto fileName = upload->customFileName().isEmpty() ? upload->fileInfo().fileName()
: upload->customFileName();
QString reqId = requestUploadSlot(upload->fileInfo(), fileName, {});
upload->setRequestId(reqId);
if (reqId.isEmpty()) {
m_runningJobs--;
m_uploads.removeAll(upload);
emit uploadFailed(upload);
delete upload;
upload->deleteLater();
// start next upload, if this wasn't a parallel upload
if (m_runningJobs == 0)
......@@ -314,7 +311,8 @@ void QXmppUploadManager::handleSlot(const QXmppHttpUploadSlotIq &slot)
if (upload->requestId() != slot.id())
continue;
if (!m_httpAllowed && !slot.hasHttpsUrls()) {
if (!m_httpAllowed && (slot.getUrl().scheme() == "http" ||
slot.putUrl().scheme() == "http")) {
m_runningJobs--;
m_uploads.removeAll(upload);
emit uploadFailed(upload);
......
......@@ -33,12 +33,12 @@
#include <QFile>
#include <QFileInfo>
#include <QNetworkReply>
#include <QMutex>
#include "QXmppUploadRequestManager.h"
#include <QNetworkReply>
#include <QXmppHttpUploadIq.h>
#include <QXmppUploadRequestManager.h>
class QNetworkAccessManager;
class QXmppHttpUploadRequestIq;
class QXmppUploadManager; // needed for QXmppHttpUpload
/// \class QXmppHttpUpload Represents a single HTTP file upload.
......
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QFileInfo>
#include <QMimeDatabase>
#include <QXmppClient.h>
#include <QXmppDiscoveryManager.h>
#include <QXmppDiscoveryIq.h>
#include <QXmppDataForm.h>
#include "QXmppUploadRequestManager.h"
QString QXmppUploadService::jid() const
{
return m_jid;
}
void QXmppUploadService::setJid(const QString &jid)
{
m_jid = jid;
}
qint64 QXmppUploadService::sizeLimit() const
{
return m_sizeLimit;
}
void QXmppUploadService::setSizeLimit(qint64 sizeLimit)
{
m_sizeLimit = sizeLimit;
}
/// Requests an upload slot from the server. Better use \see uploadFile to also upload the file.
///
/// \param file The info of the file to be uploaded.
/// \param customFileName If not empty, this name is used instead of the file's name for requesting
/// the upload slot.
/// \return The id of the sent IQ. If sendPacket() isn't successful or there has no upload service
/// been found, an empty string will be returned.
QString QXmppUploadRequestManager::requestUploadSlot(const QFileInfo &file, QString customFileName)
{
if (!serviceFound())
return "";
QMimeDatabase mimeDb;
QXmppHttpUploadRequestIq iq;
iq.setFrom(client()->configuration().jid());
iq.setTo(m_uploadServices.first().jid());
iq.setType(QXmppIq::Get);
iq.setFileName(customFileName.isEmpty() ? file.fileName() : customFileName);
iq.setSize(file.size());
iq.setContentType(mimeDb.mimeTypeForFile(file));
bool sent = client()->sendPacket(iq);
if (sent)
return iq.id();
return "";
}
bool QXmppUploadRequestManager::serviceFound() const
{
return !m_uploadServices.isEmpty();
}
bool QXmppUploadRequestManager::handleStanza(const QDomElement &element)
{
if (QXmppHttpUploadSlotIq::isHttpUploadSlotIq(element)) {
QXmppHttpUploadSlotIq slot;
slot.parse(element);
emit slotReceived(slot);
return true;
} else if (QXmppHttpUploadRequestIq::isHttpUploadRequestIq(element)) {
QXmppHttpUploadRequestIq requestError;
requestError.parse(element);
emit requestFailed(requestError);
return true;
}
return false;
}
void QXmppUploadRequestManager::handleDiscoInfo(const QXmppDiscoveryIq &iq)
{
if (!iq.features().contains(ns_httpupload))
return;
const QList<QXmppDiscoveryIq::Identity> identities = iq.identities();
for (const auto &identity : identities) {
if (identity.category() == "store" && identity.type() == "file")
goto addService;
}
return;
addService:
QXmppUploadService service;
service.setJid(iq.from());
// get size limit
bool isFormNsCorrect = false;
for (const QXmppDataForm::Field &field : qAsConst(iq.form().fields())) {
if (field.key() == "FORM_TYPE") {
isFormNsCorrect = field.value() == ns_httpupload;
} else if (isFormNsCorrect && field.key() == "max-file-size") {
service.setSizeLimit(field.value().toULongLong());
break;
}
}
m_uploadServices.append(service);
emit serviceFoundChanged();
}
void QXmppUploadRequestManager::setClient(QXmppClient *client)
{
QXmppClientExtension::setClient(client);
// connect to service discovery manager
auto *disco = client->findExtension<QXmppDiscoveryManager>();
if (disco) {
// scan info of all entities for upload services
// Could this lead to another client being added as upload service?
// Is that a vulnarability?
connect(disco, &QXmppDiscoveryManager::infoReceived,
this, &QXmppUploadRequestManager::handleDiscoInfo);
// on client disconnect remove all upload services
connect(client, &QXmppClient::disconnected, this, [=] () {
m_uploadServices.clear();
emit serviceFoundChanged();
});
}
}
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2020 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan 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.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan 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 Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QXMPPUPLOADREQUESTMANAGER_H
#define QXMPPUPLOADREQUESTMANAGER_H
#include <QXmppClientExtension.h>
#include "QXmppHttpUploadIq.h"
class QFileInfo;
class QXmppUploadService
{
public:
QString jid() const;
void setJid(const QString &jid);
qint64 sizeLimit() const;
void setSizeLimit(qint64 sizeLimit);
private:
QString m_jid;
qint64 m_sizeLimit = -1;
};
/// \class QXmppUploadRequestManager This class implements the core of XEP-0369: HTTP File Upload.
/// It handles the discovery of \see QXmppUploadService and can send upload requests and outputs the
/// upload slots. It doesn't do the actual upload via. HTTP, \see QXmppUploadManager for that
/// purpose.
class QXmppUploadRequestManager : public QXmppClientExtension
{
Q_OBJECT
Q_PROPERTY(bool serviceFound READ serviceFound NOTIFY serviceFoundChanged)
public:
QString requestUploadSlot(const QFileInfo &file, QString customFileName = QString());
bool serviceFound() const;
signals:
/// Emitted when an upload slot was received.
void slotReceived(const QXmppHttpUploadSlotIq &slot);
/// Emitted when the slot request failed.
void requestFailed(const QXmppHttpUploadRequestIq &request);
void serviceFoundChanged();
protected:
void setClient(QXmppClient *client) override;
bool handleStanza(const QDomElement &stanza) override;
private:
void handleDiscoInfo(const QXmppDiscoveryIq &iq);
QList<QXmppUploadService> m_uploadServices;
};
#endif // QXMPPUPLOADREQUESTMANAGER_H
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment