Commit 755347dd authored by Guillaume Roguez's avatar Guillaume Roguez Committed by Olivier SOLDANO

data transfer: first implementation

First implementation of Reliable Data Transfer feature in Ring.

This implementation is a draft, comes with a Python script
tools/dringctrl/sendfile.py to play with and doesn't
implement all the API as described into "datatransfer: API proposal"
commit.
This version uses TLS over TCP-TURN sockets to encrypt data.

Transfers require a TURN server with TCP peer connections
as described by the RFC-6062.

Testing: Currently only sendFile API is implemented and data are
saved into a temporary file saved in "/tmp/ring_XXXXXX",
where XXXXXX are replace by mkstemp() command.

Change-Id: I5b8f48432edd58df5046e368a99f58ea44046dcd
Reviewed-by: default avatarOlivier Soldano <olivier.soldano@savoirfairelinux.com>
parent bdafdfb4
......@@ -1395,6 +1395,29 @@
</arg>
</method>
<method name="sendFile" tp:name-for-bindings="sendFile">
<tp:added version="4.2.0"/>
<arg type="t" name="DataTransferId" direction="out">
</arg>
<arg type="s" name="accountID" direction="in">
</arg>
<arg type="s" name="peer_uri" direction="in">
<tp:docstring>
RingID of request's recipient.
</tp:docstring>
</arg>
<arg type="s" name="file_path" direction="in"></arg>
<arg type="s" name="display_name" direction="in"></arg>
</method>
<method name="dataTransferInfo" tp:name-for-bindings="dataTransferInfo">
<tp:added version="4.2.0"/>
<arg type="(buttss)" name="DataTransferInfo" direction="out">
</arg>
<arg type="t" name="DataTransferId" direction="in">
</arg>
</method>
<signal name="mediaParametersChanged" tp:name-for-bindings="mediaParametersChanged">
<tp:added version="2.3.0"/>
<tp:docstring>
......@@ -1436,6 +1459,21 @@
</arg>
</signal>
<signal name="dataTransferEvent" tp:name-for-bindings="dataTransferEvent">
<tp:added version="4.2.0"/>
<tp:docstring>
Notify clients when a data transfer state change.
</tp:docstring>
<arg type="t" name="id">
<tp:docstring>
Data transfer unique id.
</tp:docstring>
</arg>
<arg type="i" name="code">
<tp:docstring>
A DRing::DataTransferEventCode code
</tp:docstring>
</arg>
</signal>
</interface>
</node>
......@@ -42,6 +42,8 @@
#include "dbuspresencemanager.h"
#include "presencemanager_interface.h"
#include "datatransfer_interface.h"
#ifdef RING_VIDEO
#include "dbusvideomanager.h"
#include "videomanager_interface.h"
......@@ -128,6 +130,7 @@ DBusClient::initLibrary(int flags)
using DRing::ConfigurationSignal;
using DRing::PresenceSignal;
using DRing::AudioSignal;
using DRing::DataTransferSignal;
using SharedCallback = std::shared_ptr<DRing::CallbackWrapperBase>;
......@@ -203,6 +206,10 @@ DBusClient::initLibrary(int flags)
exportable_callback<AudioSignal::DeviceEvent>(bind(&DBusConfigurationManager::audioDeviceEvent, confM)),
};
const std::map<std::string, SharedCallback> dataXferEvHandlers = {
exportable_callback<DataTransferSignal::DataTransferEvent>(bind(&DBusConfigurationManager::dataTransferEvent, confM, _1, _2)),
};
#ifdef RING_VIDEO
// Video event handlers
const std::map<std::string, SharedCallback> videoEvHandlers = {
......@@ -219,6 +226,7 @@ DBusClient::initLibrary(int flags)
registerConfHandlers(configEvHandlers);
registerPresHandlers(presEvHandlers);
registerPresHandlers(audioEvHandlers);
registerDataXferHandlers(dataXferEvHandlers);
#ifdef RING_VIDEO
registerVideoHandlers(videoEvHandlers);
#endif
......
......@@ -24,6 +24,7 @@
#include "dbusconfigurationmanager.h"
#include "configurationmanager_interface.h"
#include "datatransfer_interface.h"
#include "media/audio/audiolayer.h"
......@@ -612,3 +613,24 @@ DBusConfigurationManager::connectivityChanged()
{
DRing::connectivityChanged();
}
auto
DBusConfigurationManager::sendFile(const std::string& account_id, const std::string& peer_uri,
const std::string& file_path, const std::string& display_name) -> decltype(DRing::sendFile(account_id, peer_uri, file_path, display_name))
{
return DRing::sendFile(account_id, peer_uri, file_path, display_name);
}
DBus::Struct<bool, uint32_t, uint64_t, uint64_t, std::string, std::string>
DBusConfigurationManager::dataTransferInfo(const DRing::DataTransferId& id)
{
DBus::Struct<bool, uint32_t, uint64_t, uint64_t, std::string, std::string> out;
auto info = DRing::dataTransferInfo(id);
out._1 = info.isOutgoing;
out._2 = uint32_t(info.lastEvent);
out._3 = info.totalSize;
out._4 = info.bytesProgress;
out._5 = info.displayName;
out._6 = info.path;
return out;
}
......@@ -31,6 +31,8 @@
#include "dbus_cpp.h"
#include "dring/datatransfer_interface.h"
#if __GNUC__ >= 5 || (__GNUC__ >=4 && __GNUC_MINOR__ >= 6)
/* This warning option only exists for gcc 4.6.0 and greater. */
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
......@@ -151,6 +153,9 @@ class DBusConfigurationManager :
int exportAccounts(const std::vector<std::string>& accountIDs, const std::string& filepath, const std::string& password);
int importAccounts(const std::string& archivePath, const std::string& password);
void connectivityChanged();
DRing::DataTransferId sendFile(const std::string& account_id, const std::string& peer_uri,
const std::string& file_path, const std::string& display_name);
DBus::Struct<bool, uint32_t, uint64_t, uint64_t, std::string, std::string> dataTransferInfo(const DRing::DataTransferId& id);
};
#endif // __RING_DBUSCONFIGURATIONMANAGER_H__
......@@ -2,7 +2,7 @@ dnl Ring - configure.ac for automake 1.9 and autoconf 2.59
dnl Process this file with autoconf to produce a configure script.
AC_PREREQ([2.65])
AC_INIT([Ring Daemon],[4.1.0],[ring@gnu.org],[ring])
AC_INIT([Ring Daemon],[4.2.0],[ring@gnu.org],[ring])
AC_COPYRIGHT([[Copyright (c) Savoir-faire Linux 2004-2017]])
AC_REVISION([$Revision$])
......
......@@ -31,7 +31,7 @@ PROJECT_NAME = "Ring Daemon"
# This could be handy for archiving the generated documentation or
# if some version control system is used.
PROJECT_NUMBER = 4.1.0
PROJECT_NUMBER = 4.2.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer
......
......@@ -139,12 +139,18 @@ libring_la_SOURCES = \
rational.h \
smartools.cpp \
smartools.h \
base64.h \
base64.cpp \
turn_transport.h \
turn_transport.cpp \
channel.h \
generic_io.h
base64.h \
base64.cpp \
turn_transport.h \
turn_transport.cpp \
channel.h \
peer_connection.cpp \
peer_connection.h \
data_transfer.cpp \
data_transfer.h \
ftp_server.cpp \
ftp_server.h \
generic_io.h
if HAVE_WIN32
libring_la_SOURCES += \
......
......@@ -104,7 +104,7 @@ Account::Account(const std::string &accountID)
{
random_device rdev;
std::seed_seq seed {rdev(), rdev()};
rand_.seed(seed);
rand.seed(seed);
// Initialize the codec order, used when creating a new account
loadDefaultCodecs();
......
......@@ -296,7 +296,7 @@ class Account : public Serializable, public std::enable_shared_from_this<Account
* Random generator engine
* Logical account state shall never rely on the state of the random generator.
*/
mutable std::mt19937_64 rand_;
mutable std::mt19937_64 rand;
/**
* Inform the account that the network status has changed.
......
......@@ -16,6 +16,7 @@ libclient_la_SOURCES = \
ring_signal.cpp \
callmanager.cpp \
configurationmanager.cpp \
datatransfer.cpp \
$(PRESENCE_SRC) \
$(VIDEO_SRC)
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "datatransfer_interface.h"
#include "manager.h"
#include "data_transfer.h"
#include "client/ring_signal.h"
namespace DRing {
void
registerDataXferHandlers(const std::map<std::string, std::shared_ptr<CallbackWrapperBase>>& handlers)
{
auto& handlers_ = ring::getSignalHandlers();
for (const auto& item : handlers) {
auto iter = handlers_.find(item.first);
if (iter == handlers_.end()) {
RING_ERR("Signal %s not supported", item.first.c_str());
continue;
}
iter->second = std::move(item.second);
}
}
DataTransferId
sendFile(const std::string& account_id,
const std::string& peer_uri,
const std::string& file_path,
const std::string& display_name)
{
return ring::Manager::instance().dataTransfers->sendFile(
account_id, peer_uri, file_path, display_name.empty() ? file_path : display_name);
}
void
acceptFileTransfer(const DataTransferId& id,
const std::string& file_path,
std::size_t offset)
{
ring::Manager::instance().dataTransfers->acceptAsFile(id, file_path, offset);
}
void
cancelDataTransfer(const DataTransferId& id)
{
ring::Manager::instance().dataTransfers->cancel(id);
}
std::streamsize
dataTransferBytesSent(const DataTransferId& id)
{
return ring::Manager::instance().dataTransfers->bytesSent(id);
}
DataTransferInfo
dataTransferInfo(const DataTransferId& id)
{
return ring::Manager::instance().dataTransfers->info(id);
}
} // namespace DRing
......@@ -93,6 +93,9 @@ getSignalHandlers()
/* Audio */
exported_callback<DRing::AudioSignal::DeviceEvent>(),
/* DataTransfer */
exported_callback<DRing::DataTransferSignal::DataTransferEvent>(),
#ifdef RING_VIDEO
/* Video */
exported_callback<DRing::VideoSignal::DeviceEvent>(),
......
......@@ -27,6 +27,7 @@
#include "callmanager_interface.h"
#include "configurationmanager_interface.h"
#include "presencemanager_interface.h"
#include "datatransfer_interface.h"
#ifdef RING_VIDEO
#include "videomanager_interface.h"
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "data_transfer.h"
#include "manager.h"
#include "ringdht/ringaccount.h"
#include "peer_connection.h"
#include "fileutils.h"
#include "string_utils.h"
#include "client/ring_signal.h"
#include <stdexcept>
#include <fstream>
#include <sstream>
#include <ios>
#include <iostream>
#include <unordered_map>
#include <mutex>
#include <future>
#include <atomic>
#include <cstdlib> // mkstemp
namespace ring {
static DRing::DataTransferId
generateUID()
{
static DRing::DataTransferId lastId = 0;
return lastId++;
}
//==============================================================================
class DataTransfer : public Stream
{
public:
DataTransfer(DRing::DataTransferId id) : Stream(), id {id} {}
virtual ~DataTransfer() = default;
DRing::DataTransferId getId() const override {
return id;
}
virtual void accept(const std::string&, std::size_t) {};
virtual bool start() {
bool expected = false;
return started_.compare_exchange_strong(expected, true);
}
virtual std::streamsize bytesSent() const {
std::lock_guard<std::mutex> lk {infoMutex_};
return info_.bytesProgress;
}
DRing::DataTransferInfo info() const {
std::lock_guard<std::mutex> lk {infoMutex_};
return info_;
}
void emit(DRing::DataTransferEventCode code) const;
const DRing::DataTransferId id;
protected:
mutable std::mutex infoMutex_;
mutable DRing::DataTransferInfo info_;
std::atomic_bool started_ {false};
};
void
DataTransfer::emit(DRing::DataTransferEventCode code) const
{
{
std::lock_guard<std::mutex> lk {infoMutex_};
info_.lastEvent = code;
}
emitSignal<DRing::DataTransferSignal::DataTransferEvent>(id, uint32_t(code));
}
//==============================================================================
class FileTransfer final : public DataTransfer
{
public:
FileTransfer(DRing::DataTransferId id, const std::string&, const std::string&);
bool start() override;
void close() noexcept override;
bool read(std::vector<uint8_t>&) const override;
private:
FileTransfer() = delete;
mutable std::ifstream input_;
mutable std::size_t tx_ {0};
mutable bool headerSent_ {false};
const std::string peerUri_;
};
FileTransfer::FileTransfer(DRing::DataTransferId id,
const std::string& file_path,
const std::string& display_name)
: DataTransfer(id)
{
input_.open(file_path, std::ios::binary);
if (!input_)
throw std::runtime_error("input file open failed");
info_.isOutgoing = true;
info_.displayName = display_name;
info_.path = file_path;
// File size?
input_.seekg(0, std::ios_base::end);
info_.totalSize = input_.tellg();
input_.seekg(0, std::ios_base::beg);
emit(DRing::DataTransferEventCode::created);
}
bool
FileTransfer::start()
{
if (DataTransfer::start()) {
emit(DRing::DataTransferEventCode::ongoing);
return true;
}
return false;
}
void
FileTransfer::close() noexcept
{
input_.close();
if (info_.lastEvent < DRing::DataTransferEventCode::finished)
emit(DRing::DataTransferEventCode::closed_by_host);
}
bool
FileTransfer::read(std::vector<uint8_t>& buf) const
{
if (!headerSent_) {
std::stringstream ss;
ss << "Content-Length: " << info_.totalSize << '\n'
<< "Display-Name: " << info_.displayName << '\n'
<< "Offset: 0\n"
<< '\n';
auto header = ss.str();
buf.resize(header.size());
std::copy(std::begin(header), std::end(header), std::begin(buf));
headerSent_ = true;
return true;
}
input_.read(reinterpret_cast<char*>(&buf[0]), buf.size());
auto n = input_.gcount();
buf.resize(n);
{
std::lock_guard<std::mutex> lk {infoMutex_};
info_.bytesProgress += n;
}
if (n)
return true;
if (input_.eof()) {
RING_DBG() << "FTP#" << getId() << ": sent " << info_.bytesProgress << " bytes";
emit(DRing::DataTransferEventCode::finished);
return false;
} else {
throw std::runtime_error("FileTransfer IO read failed"); // TODO: better exception?
}
return true;
}
//==============================================================================
class IncomingFileTransfer final : public DataTransfer
{
public:
IncomingFileTransfer(DRing::DataTransferId id, const std::string&, std::size_t);
bool start() override;
void close() noexcept override;
std::string requestFilename();
void accept(const std::string&, std::size_t offset) override;
private:
IncomingFileTransfer() = delete;
std::promise<void> filenamePromise_;
};
IncomingFileTransfer::IncomingFileTransfer(DRing::DataTransferId id,
const std::string& display_name,
std::size_t offset)
: DataTransfer(id)
{
RING_WARN() << "[FTP] incoming transfert: " << display_name;
(void)offset;
info_.isOutgoing = false;
info_.displayName = display_name;
// TODO: use offset?
emit(DRing::DataTransferEventCode::created);
}
std::string
IncomingFileTransfer::requestFilename()
{
emit(DRing::DataTransferEventCode::wait_host_acceptance);
// Now wait for DataTransferFacade::acceptFileTransfer() call
#if 0
filenamePromise_.get_future().wait();
return info_.path;
#else
// DEBUG
char filename[] = "/tmp/ring_XXXXXX";
if (::mkstemp(filename) < 0)
throw std::system_error(errno, std::generic_category());
return filename;
#endif
}
bool
IncomingFileTransfer::start()
{
if (DataTransfer::start()) {
emit(DRing::DataTransferEventCode::ongoing);
return true;
}
return false;
}
void
IncomingFileTransfer::close() noexcept
{
filenamePromise_.set_value();
}
void
IncomingFileTransfer::accept(const std::string& filename, std::size_t offset)
{
// TODO: offset?
(void)offset;
info_.path = filename;
filenamePromise_.set_value();
start();
}
//==============================================================================
class DataTransferFacade::Impl
{
public:
mutable std::mutex mapMutex_;
std::unordered_map<DRing::DataTransferId, std::shared_ptr<DataTransfer>> map_;
std::shared_ptr<DataTransfer> createFileTransfer(const std::string& file_path,
const std::string& display_name);
std::shared_ptr<IncomingFileTransfer> createIncomingFileTransfer(const std::string& display_name,
std::size_t offset);
std::shared_ptr<DataTransfer> getTransfer(const DRing::DataTransferId& id);
void cancel(DataTransfer& transfer);
void onConnectionRequestReply(const DRing::DataTransferId& id, PeerConnection* connection);
};
void DataTransferFacade::Impl::cancel(DataTransfer& transfer)
{
transfer.close();
map_.erase(transfer.getId());
}
std::shared_ptr<DataTransfer>
DataTransferFacade::Impl::getTransfer(const DRing::DataTransferId& id)
{
std::lock_guard<std::mutex> lk {mapMutex_};
const auto& iter = map_.find(id);
if (iter == std::end(map_))
return {};
return iter->second;
}
std::shared_ptr<DataTransfer>
DataTransferFacade::Impl::createFileTransfer(const std::string& file_path,