Verified Commit 36330e99 authored by Linus Jahn's avatar Linus Jahn 🔌 Committed by Linus Jahn
Browse files

Implement XEP-0363: HTTP File Upload Manager & Handlers

parent a3f3a307
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2018 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 GLOOXEXTS_H__
#define GLOOXEXTS_H__
#include <string>
#include <map>
namespace gloox {
static const std::string XMLNS_HTTPUPLOAD = "urn:xmpp:http:upload:0";
static const int EXT_HTTPUPLOADREQUEST = 0x10AD; //=> 4269
static const int EXT_HTTPUPLOADSLOT = 4270;
typedef std::map<std::string, std::string> HeaderFieldMap;
enum HttpUploadState {
UploadNone, /* No state */
UploadWaiting, /* Upload is waiting in queue */
UploadRequested, /* An upload slot has been requested */
UploadInProgress /* The upload is in progress */
};
enum HttpUploadError {
UploadNoError, /* No error occured */
UploadUnknownError, /* An unknown error occured */
UploadTooLarge, /* The requested file is too large */
UploadQuotaReached, /* The upload quota was reached, try again later */
UploadNotAllowed, /* The upload service doesn't allow uploads from us */
UploadFileNotFound, /* Couldn't find or read file on disk */
UploadHttpError /* Error occured while uploading via HTTPS */
};
}
#endif // GLOOXEXTS_H__
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2018 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 HTTPUPLOADER_H__
#define HTTPUPLOADER_H__
#include <string>
#include <map>
#include <gloox/gloox.h>
#include "gloox-extensions.h"
namespace gloox {
class HttpUploadManager;
/**
* @class HttpUploader A virtual interface for implementing an HTTP Uploader
* that can be used with @c HttpUploadManager.
*
* This class is only used for uploading files from disk to the server.
*/
class GLOOX_API HttpUploader
{
public:
/**
* Uploads a file to an HTTP PUT url from a local path
*/
virtual void uploadFile(int id, std::string putUrl,
HeaderFieldMap putHeaders,
std::string localPath) = 0;
/**
* Returns true, if supporting multiple uploads at the same time
*/
virtual bool supportsParallel() = 0;
/**
* Must return true, while uploading, even if parrallel uploads are
* supported
*/
virtual bool busy() = 0;
};
}
#endif // HTTPUPLOADER_H__
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2018 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 HTTPUPLOADHANDLER_H__
#define HTTPUPLOADHANDLER_H__
#include <gloox/jid.h>
#include "httpuploadmanager.h"
#include <string.h>
namespace gloox {
class HttpUploadSlot;
/**
* @class HttpUploadHandler A virtual interface that enables objects to
* receive HTTP File Upload (@xep{0363}) events.
*/
class GLOOX_API HttpUploadHandler
{
public:
/**
* Called, when a new upload service was added.
*
* @param jid The JID of the upload service that has been added
* @param maxFileSize The maximum file size for uploading to this service
*/
virtual void handleUploadServiceAdded(const JID &jid,
unsigned long maxFileSize) = 0;
/**
* Called, when an upload server has been removed.
*
* @param jid The JID of the upload service that has been removed
*/
virtual void handleUploadServiceRemoved(const JID &jid) = 0;
/**
* Called, when the file size limit has changed.
*
* @param maxFileSize The new maximum file size for uploading
*/
virtual void handleFileSizeLimitChanged(unsigned long maxFileSize) = 0;
/**
* Called, when the uploader made progress
*
* @param id Upload job id
* @param sent Number of bytes that has been sent
* @param total Number of total bytes to upload
*/
virtual void handleUploadProcess(int id, unsigned long sent,
unsigned long total) = 0;
/**
* Called, when an upload has successfully finished
*
* @param id Upload job id
* @param getUrl HTTPS GET url to share with others and download the file
*/
virtual void handleUploadFinished(int id, std::string &getUrl) = 0;
/**
* Called, when an upload job has failed
*
* @param id Upload job id
* @param error The error that has occured
* @param text An optional message about what went wrong
* @param stamp A UTC timestamp that will show the date, when the next
* upload can be made, if the upload quota was reached.
*/
virtual void handleUploadFailed(int id, HttpUploadError error,
const std::string &text = EmptyString,
const std::string &stamp = EmptyString) = 0;
};
}
#endif // HTTPUPLOADHANDLER_H__
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2017-2018 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 "httpuploadmanager.h"
#include "httpuploadhandler.h"
#include "httpuploadrequest.h"
#include "httpuploadslot.h"
#include "httpuploader.h"
#include <gloox/dataform.h>
#include <gloox/dataformitem.h>
#include <gloox/dataformreported.h>
#include <gloox/error.h>
#include <fstream>
#include <sys/stat.h>
using namespace gloox;
HttpUploadManager::HttpUploadManager(Client *client)
: client(client)
{
}
HttpUploadManager::~HttpUploadManager()
{
client->removeIDHandler(this);
}
bool HttpUploadManager::valid()
{
return uploader && handler && uploadServices.size() > 0;
}
int HttpUploadManager::uploadFile(std::string &path, bool queue,
std::string contentType, std::string name,
unsigned long length)
{
if (!valid() || !existsFile(path))
return -1;
FileMeta *file = new FileMeta;
file->id = newUploadId();
file->path = path;
file->name = name.length() == 0 ? getFileName(path) : name;
file->contentType = contentType;
file->length = length == 0 ? calculateFileSize(path) : length;
file->state = UploadWaiting; // waiting in queue
uploadQueue[file->id] = file;
// if uploader allows, directly start upload
if (!uploader->busy() || (!queue && uploader->supportsParallel())) {
// directly start
file->state = UploadRequested;
sendUploadRequest(file->id, file->name, file->length, file->contentType);
}
return file->id;
}
void HttpUploadManager::tryAddFileStoreService(const JID &jid,
const Disco::Info &info)
{
// check http file upload feature
if (!info.hasFeature(XMLNS_HTTPUPLOAD))
return;
// gloox can only get the first dataform of a Disco::Info result, thus we
// need to check both the old and the new xml namespace of HTTP File Upload
const DataForm *form = info.form();
if (
form->type() == TypeResult &&
form->hasField("FORM_TYPE") &&
(form->field("FORM_TYPE")->value() == XMLNS_HTTPUPLOAD ||
form->field("FORM_TYPE")->value() == "urn:xmpp:http:upload") &&
form->hasField("max-file-size")
) {
// get old maximum file size
unsigned long oldMaxFileSize = 0;
for (UploadService &service : uploadServices) {
if (service.maxFileSize > oldMaxFileSize)
oldMaxFileSize = service.maxFileSize;
}
try {
// get max file size for uploads from form data
unsigned long maxFileSize = std::stoul(
form->field("max-file-size")->value());
// register new upload service
addUploadService(jid, maxFileSize);
// handle new upload service added
handler->handleUploadServiceAdded(jid, maxFileSize);
// handle new file size limit
if (maxFileSize > oldMaxFileSize)
handler->handleFileSizeLimitChanged(maxFileSize);
} catch (std::invalid_argument &e) {
// Couldn't parse data form field max-file-size.
} catch (std::out_of_range &e) {
// Data form field max-file-size is out of range of an unsigned long.
}
}
}
void HttpUploadManager::addUploadService(JID jid, const unsigned long maxFileSize)
{
// add the new upload service to the list (if not existant already) and sort the list
std::string bare = jid.bare();
if (!hasUploadService(bare)) {
UploadService service;
service.jid = bare;
service.maxFileSize = maxFileSize;
uploadServices.emplace_back(service);
sortUploadServices();
}
if (!uploader->busy())
startNextUpload();
}
bool HttpUploadManager::hasUploadService(std::string &jid) const
{
for (UploadService service : uploadServices) {
if (service.jid == jid)
return true;
}
return false;
}
void HttpUploadManager::sortUploadServices()
{
// this will sort the upload services after the max file size (largest first)
uploadServices.sort([] (const UploadService &a, const UploadService &b) {
return a.maxFileSize > b.maxFileSize;
});
}
void HttpUploadManager::sendUploadRequest(int id, std::string &filename,
unsigned long size, std::string &contentType)
{
JID uploadServiceJid = JID(uploadServices.front().jid);
IQ requestIq(IQ::Get, uploadServiceJid, client->getID());
HttpUploadRequest *requestExt = new HttpUploadRequest(filename, size, contentType);
requestIq.addExtension(requestExt);
client->send(requestIq, this, id, false);
}
void HttpUploadManager::handleIqID(const gloox::IQ &iq, int context)
{
HttpUploadSlot *slot = (HttpUploadSlot*) iq.findExtension(EXT_HTTPUPLOADSLOT);
if (slot && slot->valid()) {
// update state of upload to in progress
uploadQueue[context]->state = UploadInProgress;
uploadQueue[context]->getUrl = slot->getUrl();
uploader->uploadFile(
context, slot->putUrl(), slot->putHeaderFields(),
uploadQueue[context]->path
);
} else {
HttpUploadRequest *request = (HttpUploadRequest*) iq.findExtension(EXT_HTTPUPLOADREQUEST);
Error *error = (Error*) iq.findExtension(ExtError);
HttpUploadError uplError;
std::string stamp;
// if there's no slot and no request or no error, an unexpected error occured
if (!request || !error) {
uplError = UploadUnknownError;
} else if (error->type() == StanzaErrorTypeModify &&
error->error() == StanzaErrorNotAcceptable) {
uplError = UploadTooLarge;
} else if (error->type() == StanzaErrorTypeWait &&
error->error() == StanzaErrorResourceConstraint) {
uplError = UploadQuotaReached;
Tag *retry = error->tag()->findChild("retry", "xmlns", XMLNS_HTTPUPLOAD);
if (retry)
std::string stamp = retry->findAttribute("stamp");
} else if (error->type() == StanzaErrorTypeCancel &&
error->error() == StanzaErrorNotAllowed) {
uplError = UploadNotAllowed;
// remove this upload service, if we aren't permitted to use it
for (std::list<UploadService>::iterator it = uploadServices.begin();
it != uploadServices.end();) {
if (it->jid == iq.from().bare())
it = uploadServices.erase(it);
else
it++;
}
}
// get text message of error
std::string text = error->text();
handler->handleUploadFailed(context, uplError, text, stamp);
uploadQueue.erase(context);
startNextUpload();
}
}
void HttpUploadManager::uploadFinished(int id)
{
std::string getUrl = uploadQueue[id]->getUrl;
uploadQueue.erase(id);
handler->handleUploadFinished(id, getUrl);
startNextUpload();
}
void HttpUploadManager::uploadFailed(int id, HttpUploadError error)
{
handler->handleUploadFailed(id, error);
uploadQueue.erase(id);
}
void HttpUploadManager::uploadProgress(int id, unsigned long sent, unsigned long total)
{
// if total upload size is not set, use length of local file
handler->handleUploadProcess(id, sent, total != 0 ? total :
uploadQueue[id]->length);
}
void HttpUploadManager::startNextUpload()
{
for (const auto &pair : uploadQueue) {
if (pair.second->state == UploadWaiting) {
pair.second->state = UploadRequested;
sendUploadRequest(pair.second->id, pair.second->name,
pair.second->length, pair.second->contentType);
break;
}
}
}
const unsigned long HttpUploadManager::maxFileSize()
{
unsigned long maxFileSize = 0;
for (UploadService srv : uploadServices) {
if (srv.maxFileSize > maxFileSize)
maxFileSize = srv.maxFileSize;
}
return maxFileSize;
}
bool HttpUploadManager::existsFile(const std::string &path) const
{
struct stat buffer;
return stat(path.c_str(), &buffer) == 0;
}
const unsigned long HttpUploadManager::calculateFileSize(std::string &path) const
{
try {
std::ifstream in(path.c_str(), std::ifstream::ate | std::ifstream::binary);
return in.tellg();
} catch (std::ios_base::failure &e) {
return 0;
}
}
const std::string HttpUploadManager::getFileName(std::string &path) const
{
#ifdef _WIN32
char sep = '\\';
#else
char sep = '/';
#endif
size_t i = path.rfind(sep, path.length());
if (i != std::string::npos) {
return path.substr(i + 1, path.length() - i);
}
return "";
}
/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2017-2018 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 HTTPUPLOADMANAGER_H__
#define HTTPUPLOADMANAGER_H__
#include <gloox/client.h>
#include <gloox/discohandler.h>
#include "gloox-extensions.h"
#include <string.h>
#include <list>
#include <map>
namespace gloox {
class HttpUploadHandler;
class HttpUploader;
/**
* @brief An manager that handles upload services and can upload files (@xep{0363}).
*
* First, you need to register the HTTP File Upload stanza extensions to
* make them usable in your gloox @c Client.
*
* @code
* client->registerStanzaExtension( new HttpUploadRequest() );
* client->registerStanzaExtension( new HttpUploadSlot() );
* @endcode
*
* The second step is to to create an @c HttpUploadManager and register an
* @c HttpUploader and an @c HttpUploadHandler to it. Gloox doesn't provide
* an ready to use @c HttpUploader for you, so unfortunately you need to
* implement one, first. After that we can create our @c HttpUploadHandler
* based class. Here's an example:
*
* @code
* MyUploadHandler::MyUploadHandler(Client *client)
* {