connectionmanager: prevent JamiAccount usage

GitLab: #778
Change-Id: I5e7eb072ebda81c4ae45316cc46842dafdeaad13
This commit is contained in:
Adrien Béraud
2023-06-28 13:50:15 -04:00
committed by Sébastien Blin
parent 7c7c1d38c3
commit edebe17139
8 changed files with 760 additions and 238 deletions

View File

@ -16,12 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "connectionmanager.h"
#include "jamidht/jamiaccount.h"
#include "account_const.h"
#include "jamidht/account_manager.h"
#include "manager.h"
#include "peer_connection.h"
#include "logger.h"
#include "fileutils.h"
#include "connectivity/upnp/upnp_control.h"
#include "connectivity/sip_utils.h"
#include "connectivity/security/certstore.h"
#include <asio.hpp>
#include <opendht/crypto.h>
@ -61,14 +63,31 @@ struct ConnectionInfo
std::unique_ptr<asio::steady_timer> waitForAnswer_ {};
};
/**
* returns whether or not UPnP is enabled and active_
* ie: if it is able to make port mappings
*/
bool
ConnectionManager::Config::getUPnPActive() const
{
std::lock_guard<std::mutex> lk(upnp_mtx);
if (upnpCtrl_)
return upnpCtrl_->isReady();
return false;
}
class ConnectionManager::Impl : public std::enable_shared_from_this<ConnectionManager::Impl>
{
public:
explicit Impl(JamiAccount& account)
: account {account}
explicit Impl(std::shared_ptr<ConnectionManager::Config> config_)
: config_ {std::move(config_)}
, rand {dht::crypto::getSeededRandomEngine<std::mt19937_64>()}
{}
~Impl() {}
std::shared_ptr<dht::DhtRunner> dht() { return config_->dht_; }
const dht::crypto::Identity& identity() const { return config_->id_; }
void removeUnusedConnections(const DeviceId& deviceId = {})
{
std::vector<std::shared_ptr<ConnectionInfo>> unused {};
@ -163,6 +182,87 @@ public:
void onPeerResponse(const PeerConnectionRequest& req);
void onDhtConnected(const dht::crypto::PublicKey& devicePk);
const std::shared_future<tls::DhParams> dhParams() const;
tls::CertificateStore& certStore() const { return *config_->certStore_; }
mutable std::mutex messageMutex_ {};
std::set<std::string, std::less<>> treatedMessages_ {};
void loadTreatedMessages();
void saveTreatedMessages() const;
/// \return true if the given DHT message identifier has been treated
/// \note if message has not been treated yet this method store this id and returns true at
/// further calls
bool isMessageTreated(std::string_view id);
/**
* Published IPv4/IPv6 addresses, used only if defined by the user in account
* configuration
*
*/
IpAddr publishedIp_[2] {};
// This will be stored in the configuration
std::string publishedIpAddress_ {};
/**
* Published port, used only if defined by the user
*/
pj_uint16_t publishedPort_ {sip_utils::DEFAULT_SIP_PORT};
/**
* interface name on which this account is bound
*/
std::string interface_ {"default"};
/**
* Get the local interface name on which this account is bound.
*/
const std::string& getLocalInterface() const { return interface_; }
/**
* Get the published IP address, fallbacks to NAT if family is unspecified
* Prefers the usage of IPv4 if possible.
*/
IpAddr getPublishedIpAddress(uint16_t family = PF_UNSPEC) const;
/**
* Set published IP address according to given family
*/
void setPublishedAddress(const IpAddr& ip_addr);
/**
* Store the local/public addresses used to register
*/
void storeActiveIpAddress(std::function<void()>&& cb = {});
/**
* Create and return ICE options.
*/
void getIceOptions(std::function<void(IceTransportOptions&&)> cb) noexcept;
IceTransportOptions getIceOptions() const noexcept;
/**
* Inform that a potential peer device have been found.
* Returns true only if the device certificate is a valid device certificate.
* In that case (true is returned) the account_id parameter is set to the peer account ID.
*/
static bool foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& crt,
dht::InfoHash& account_id);
bool findCertificate(const dht::PkId& id,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb);
bool findCertificate(
const dht::InfoHash& h,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb = {});
/**
* returns whether or not UPnP is enabled and active
* ie: if it is able to make port mappings
*/
bool getUPnPActive() const;
/**
* Triggered when a new TLS socket is ready to use
* @param ok If succeed
@ -175,7 +275,11 @@ public:
const dht::Value::Id& vid,
const std::string& name = "");
JamiAccount& account;
std::shared_ptr<ConnectionManager::Config> config_;
mutable std::mt19937_64 rand;
iOSConnectedCallback iOSConnectedCb_ {};
std::mutex infosMtx_ {};
// Note: Someone can ask multiple sockets, so to avoid any race condition,
@ -348,17 +452,16 @@ ConnectionManager::Impl::connectDeviceStartIce(
value->user_type = "peer_request";
// Send connection request through DHT
JAMI_DBG() << account << "Request connection to " << deviceId;
account.dht()->putEncrypted(
dht::InfoHash::get(PeerConnectionRequest::key_prefix + devicePk->getId().toString()),
devicePk,
value,
[deviceId, accId = account.getAccountID()](bool ok) {
JAMI_DEBUG("[Account {:s}] Send connection request to {:s}. Put encrypted {:s}",
accId,
deviceId.toString(),
(ok ? "ok" : "failed"));
});
JAMI_DBG() << "Request connection to " << deviceId;
dht()->putEncrypted(dht::InfoHash::get(PeerConnectionRequest::key_prefix
+ devicePk->getId().toString()),
devicePk,
value,
[deviceId](bool ok) {
JAMI_DEBUG("Sent connection request to {:s}. Put encrypted {:s}",
deviceId.toString(),
(ok ? "ok" : "failed"));
});
// Wait for call to onResponse() operated by DHT
if (isDestroying_) {
onConnected(true); // This avoid to wait new negotiation when destroying
@ -404,7 +507,7 @@ ConnectionManager::Impl::onResponse(const asio::error_code& ec,
auto sdp = ice->parseIceCandidates(info->response_.ice_msg);
if (not ice->startIce({sdp.rem_ufrag, sdp.rem_pwd}, std::move(sdp.rem_candidates))) {
JAMI_WARN("[Account:%s] start ICE failed", account.getAccountID().c_str());
JAMI_WARN("start ICE failed");
info->onConnected_(false);
return;
}
@ -440,13 +543,12 @@ ConnectionManager::Impl::connectDeviceOnNegoDone(
true);
// Negotiate a TLS session
JAMI_DBG() << account
<< "Start TLS session - Initied by connectDevice(). Launched by channel: " << name
JAMI_DBG() << "Start TLS session - Initied by connectDevice(). Launched by channel: " << name
<< " - device:" << deviceId << " - vid: " << vid;
info->tls_ = std::make_unique<TlsSocketEndpoint>(std::move(endpoint),
account.certStore(),
account.identity(),
account.dhParams(),
certStore(),
identity(),
dhParams(),
*cert);
info->tls_->setOnReady(
@ -466,37 +568,37 @@ ConnectionManager::Impl::connectDevice(const DeviceId& deviceId,
bool forceNewSocket,
const std::string& connType)
{
if (!account.dht()) {
if (!dht()) {
cb(nullptr, deviceId);
return;
}
if (deviceId.toString() == account.currentDeviceId()) {
if (deviceId.toString() == identity().second->getLongId().toString()) {
cb(nullptr, deviceId);
return;
}
account.findCertificate(deviceId,
[w = weak(),
deviceId,
name,
cb = std::move(cb),
noNewSocket,
forceNewSocket,
connType](const std::shared_ptr<dht::crypto::Certificate>& cert) {
if (!cert) {
JAMI_ERR("No valid certificate found for device %s",
deviceId.to_c_str());
cb(nullptr, deviceId);
return;
}
if (auto shared = w.lock()) {
shared->connectDevice(cert,
name,
std::move(cb),
noNewSocket,
forceNewSocket,
connType);
}
});
findCertificate(deviceId,
[w = weak(),
deviceId,
name,
cb = std::move(cb),
noNewSocket,
forceNewSocket,
connType](const std::shared_ptr<dht::crypto::Certificate>& cert) {
if (!cert) {
JAMI_ERR("No valid certificate found for device %s",
deviceId.to_c_str());
cb(nullptr, deviceId);
return;
}
if (auto shared = w.lock()) {
shared->connectDevice(cert,
name,
std::move(cb),
noNewSocket,
forceNewSocket,
connType);
}
});
}
void
@ -522,7 +624,7 @@ ConnectionManager::Impl::connectDevice(const std::shared_ptr<dht::crypto::Certif
cb(nullptr, deviceId);
return;
}
dht::Value::Id vid = ValueIdDist(1, JAMI_ID_MAX_VAL)(sthis->account.rand);
dht::Value::Id vid = ValueIdDist(1, JAMI_ID_MAX_VAL)(sthis->rand);
auto isConnectingToDevice = false;
{
std::lock_guard<std::mutex> lk(sthis->connectCbsMtx_);
@ -531,7 +633,7 @@ ConnectionManager::Impl::connectDevice(const std::shared_ptr<dht::crypto::Certif
const auto& pendings = pendingsIt->second;
while (pendings.connecting.find(vid) != pendings.connecting.end()
&& pendings.waiting.find(vid) != pendings.waiting.end()) {
vid = ValueIdDist(1, JAMI_ID_MAX_VAL)(sthis->account.rand);
vid = ValueIdDist(1, JAMI_ID_MAX_VAL)(sthis->rand);
}
}
// Check if already connecting
@ -580,14 +682,14 @@ ConnectionManager::Impl::connectDevice(const std::shared_ptr<dht::crypto::Certif
};
// If no socket exists, we need to initiate an ICE connection.
sthis->account.getIceOptions([w,
deviceId = std::move(deviceId),
devicePk = std::move(devicePk),
name = std::move(name),
cert = std::move(cert),
vid,
connType,
eraseInfo](auto&& ice_config) {
sthis->getIceOptions([w,
deviceId = std::move(deviceId),
devicePk = std::move(devicePk),
name = std::move(name),
cert = std::move(cert),
vid,
connType,
eraseInfo](auto&& ice_config) {
auto sthis = w.lock();
if (!sthis) {
dht::ThreadPool::io().run([eraseInfo = std::move(eraseInfo)] { eraseInfo(); });
@ -623,11 +725,11 @@ ConnectionManager::Impl::connectDevice(const std::shared_ptr<dht::crypto::Certif
});
};
ice_config.onNegoDone = [w,
deviceId = std::move(deviceId),
name = std::move(name),
cert = std::move(cert),
vid,
eraseInfo](bool ok) {
deviceId,
name,
cert = std::move(cert),
vid,
eraseInfo](bool ok) {
dht::ThreadPool::io().run([w = std::move(w),
deviceId = std::move(deviceId),
name = std::move(name),
@ -650,10 +752,9 @@ ConnectionManager::Impl::connectDevice(const std::shared_ptr<dht::crypto::Certif
}
std::unique_lock<std::mutex> lk {info->mutex_};
ice_config.master = false;
ice_config.streamsCount = JamiAccount::ICE_STREAMS_COUNT;
ice_config.compCountPerStream = JamiAccount::ICE_COMP_COUNT_PER_STREAM;
info->ice_ = Manager::instance().getIceTransportFactory().createUTransport(
sthis->account.getAccountID());
ice_config.streamsCount = 1;
ice_config.compCountPerStream = 1;
info->ice_ = Manager::instance().getIceTransportFactory().createUTransport("");
if (!info->ice_) {
JAMI_ERR("Cannot initialize ICE session.");
eraseInfo();
@ -715,7 +816,7 @@ void
ConnectionManager::Impl::onPeerResponse(const PeerConnectionRequest& req)
{
auto device = req.owner->getLongId();
JAMI_INFO() << account << " New response received from " << device.to_c_str();
JAMI_INFO() << "New response received from " << device.to_c_str();
if (auto info = getInfo(device, req.id)) {
std::lock_guard<std::mutex> lk {info->mutex_};
info->responseReceived_ = true;
@ -727,26 +828,24 @@ ConnectionManager::Impl::onPeerResponse(const PeerConnectionRequest& req)
device,
req.id));
} else {
JAMI_WARN() << account << " respond received, but cannot find request";
JAMI_WARN() << "Respond received, but cannot find request";
}
}
void
ConnectionManager::Impl::onDhtConnected(const dht::crypto::PublicKey& devicePk)
{
if (!account.dht()) {
if (!dht())
return;
}
account.dht()->listen<PeerConnectionRequest>(
dht()->listen<PeerConnectionRequest>(
dht::InfoHash::get(PeerConnectionRequest::key_prefix + devicePk.getId().toString()),
[w = weak()](PeerConnectionRequest&& req) {
auto shared = w.lock();
if (!shared)
return false;
if (shared->account.isMessageTreated(to_hex_string(req.id))) {
// Message already treated. Just ignore
// Message already treated. Just ignore
if (shared->isMessageTreated(to_hex_string(req.id)))
return true;
}
if (req.isAnswer) {
JAMI_DBG() << "Received request answer from " << req.owner->getLongId();
} else {
@ -756,7 +855,7 @@ ConnectionManager::Impl::onDhtConnected(const dht::crypto::PublicKey& devicePk)
shared->onPeerResponse(req);
} else {
// Async certificate checking
shared->account.findCertificate(
shared->findCertificate(
req.from,
[w, req = std::move(req)](
const std::shared_ptr<dht::crypto::Certificate>& cert) mutable {
@ -764,21 +863,15 @@ ConnectionManager::Impl::onDhtConnected(const dht::crypto::PublicKey& devicePk)
if (!shared)
return;
dht::InfoHash peer_h;
if (AccountManager::foundPeerDevice(cert, peer_h)) {
if (foundPeerDevice(cert, peer_h)) {
#if TARGET_OS_IOS
if ((req.connType == "videoCall" || req.connType == "audioCall")
&& jami::Manager::instance().isIOSExtension) {
bool hasVideo = req.connType == "videoCall";
emitSignal<libjami::ConversationSignal::CallConnectionRequest>(
shared->account.getAccountID(), peer_h.toString(), hasVideo);
if (shared->iOSConnectedCb_(req.connType, peer_h))
return;
}
#endif
shared->onDhtPeerRequest(req, cert);
} else {
JAMI_WARN()
<< shared->account << "Rejected untrusted connection request from "
<< req.owner->getLongId();
JAMI_WARN() << "Rejected untrusted connection request from "
<< req.owner->getLongId();
}
});
}
@ -860,17 +953,16 @@ ConnectionManager::Impl::answerTo(IceTransport& ice,
auto value = std::make_shared<dht::Value>(std::move(val));
value->user_type = "peer_request";
JAMI_DBG() << account << "[CNX] connection accepted, DHT reply to " << from->getLongId();
account.dht()->putEncrypted(
dht::InfoHash::get(PeerConnectionRequest::key_prefix + from->getId().toString()),
from,
value,
[from, accId = account.getAccountID()](bool ok) {
JAMI_DEBUG("[Account {:s}] Answer to connection request from {:s}. Put encrypted {:s}",
accId,
from->getLongId().toString(),
(ok ? "ok" : "failed"));
});
JAMI_DBG() << "[CNX] connection accepted, DHT reply to " << from->getLongId();
dht()->putEncrypted(dht::InfoHash::get(PeerConnectionRequest::key_prefix
+ from->getId().toString()),
from,
value,
[from](bool ok) {
JAMI_DEBUG("Answer to connection request from {:s}. Put encrypted {:s}",
from->getLongId().toString(),
(ok ? "ok" : "failed"));
});
}
bool
@ -893,7 +985,7 @@ ConnectionManager::Impl::onRequestStartIce(const PeerConnectionRequest& req)
auto sdp = ice->parseIceCandidates(req.ice_msg);
answerTo(*ice, req.id, req.owner);
if (not ice->startIce({sdp.rem_ufrag, sdp.rem_pwd}, std::move(sdp.rem_candidates))) {
JAMI_ERR("[Account:%s] start ICE failed - fallback to TURN", account.getAccountID().c_str());
JAMI_ERR("Start ICE failed - fallback to TURN");
ice = nullptr;
if (connReadyCb_)
connReadyCb_(deviceId, "", nullptr);
@ -924,18 +1016,18 @@ ConnectionManager::Impl::onRequestOnNegoDone(const PeerConnectionRequest& req)
// init TLS session
auto ph = req.from;
JAMI_DBG() << account << "Start TLS session - Initied by DHT request. Device:" << req.from
JAMI_DBG() << "Start TLS session - Initied by DHT request. Device:" << req.from
<< " - vid: " << req.id;
info->tls_ = std::make_unique<TlsSocketEndpoint>(
std::move(endpoint),
account.certStore(),
account.identity(),
account.dhParams(),
certStore(),
identity(),
dhParams(),
[ph, w = weak()](const dht::crypto::Certificate& cert) {
auto shared = w.lock();
if (!shared)
return false;
auto crt = shared->account.certStore().getCertificate(cert.getLongId().toString());
auto crt = shared->certStore().getCertificate(cert.getLongId().toString());
if (!crt)
return false;
return crt->getPacked() == cert.getPacked();
@ -954,16 +1046,14 @@ ConnectionManager::Impl::onDhtPeerRequest(const PeerConnectionRequest& req,
const std::shared_ptr<dht::crypto::Certificate>& /*cert*/)
{
auto deviceId = req.owner->getLongId();
JAMI_INFO() << account << "New connection requested by " << deviceId;
JAMI_INFO() << "New connection requested by " << deviceId;
if (!iceReqCb_ || !iceReqCb_(deviceId)) {
JAMI_INFO("[Account:%s] refuse connection from %s",
account.getAccountID().c_str(),
deviceId.toString().c_str());
JAMI_INFO("Refuse connection from %s", deviceId.toString().c_str());
return;
}
// Because the connection is accepted, create an ICE socket.
account.getIceOptions([w = weak(), req, deviceId](auto&& ice_config) {
getIceOptions([w = weak(), req, deviceId](auto&& ice_config) {
auto shared = w.lock();
if (!shared)
return;
@ -1025,15 +1115,12 @@ ConnectionManager::Impl::onDhtPeerRequest(const PeerConnectionRequest& req,
std::lock_guard<std::mutex> lk(shared->infosMtx_);
shared->infos_[{deviceId, req.id}] = info;
}
JAMI_INFO("[Account:%s] accepting connection from %s",
shared->account.getAccountID().c_str(),
deviceId.toString().c_str());
JAMI_INFO("accepting connection from %s", deviceId.toString().c_str());
std::unique_lock<std::mutex> lk {info->mutex_};
ice_config.streamsCount = JamiAccount::ICE_STREAMS_COUNT;
ice_config.compCountPerStream = JamiAccount::ICE_COMP_COUNT_PER_STREAM;
ice_config.streamsCount = 1;
ice_config.compCountPerStream = 1; // TCP
ice_config.master = true;
info->ice_ = Manager::instance().getIceTransportFactory().createUTransport(
shared->account.getAccountID());
info->ice_ = Manager::instance().getIceTransportFactory().createUTransport("");
if (not info->ice_) {
JAMI_ERR("Cannot initialize ICE session.");
eraseInfo();
@ -1094,8 +1181,296 @@ ConnectionManager::Impl::addNewMultiplexedSocket(const CallbackId& id, const std
});
}
ConnectionManager::ConnectionManager(JamiAccount& account)
: pimpl_ {std::make_shared<Impl>(account)}
const std::shared_future<tls::DhParams>
ConnectionManager::Impl::dhParams() const
{
return dht::ThreadPool::computation().get<tls::DhParams>(
std::bind(tls::DhParams::loadDhParams, fileutils::get_cache_dir() + DIR_SEPARATOR_STR "dhParams"));
;
}
template<typename ID = dht::Value::Id>
std::set<ID, std::less<>>
loadIdList(const std::string& path)
{
std::set<ID, std::less<>> ids;
std::ifstream file = fileutils::ifstream(path);
if (!file.is_open()) {
JAMI_DBG("Could not load %s", path.c_str());
return ids;
}
std::string line;
while (std::getline(file, line)) {
if constexpr (std::is_same<ID, std::string>::value) {
ids.emplace(std::move(line));
} else if constexpr (std::is_integral<ID>::value) {
ID vid;
if (auto [p, ec] = std::from_chars(line.data(), line.data() + line.size(), vid, 16);
ec == std::errc()) {
ids.emplace(vid);
}
}
}
return ids;
}
template<typename List = std::set<dht::Value::Id>>
void
saveIdList(const std::string& path, const List& ids)
{
std::ofstream file = fileutils::ofstream(path, std::ios::trunc | std::ios::binary);
if (!file.is_open()) {
JAMI_ERR("Could not save to %s", path.c_str());
return;
}
for (auto& c : ids)
file << std::hex << c << "\n";
}
void
ConnectionManager::Impl::loadTreatedMessages()
{
std::lock_guard<std::mutex> lock(messageMutex_);
auto path = config_->cachePath + DIR_SEPARATOR_STR "treatedMessages";
treatedMessages_ = loadIdList<std::string>(path);
if (treatedMessages_.empty()) {
auto messages = loadIdList(path);
for (const auto& m : messages)
treatedMessages_.emplace(to_hex_string(m));
}
}
void
ConnectionManager::Impl::saveTreatedMessages() const
{
dht::ThreadPool::io().run([w = weak()]() {
if (auto sthis = w.lock()) {
auto& this_ = *sthis;
std::lock_guard<std::mutex> lock(this_.messageMutex_);
fileutils::check_dir(this_.config_->cachePath.c_str());
saveIdList<decltype(this_.treatedMessages_)>(this_.config_->cachePath
+ DIR_SEPARATOR_STR "treatedMessages",
this_.treatedMessages_);
}
});
}
bool
ConnectionManager::Impl::isMessageTreated(std::string_view id)
{
std::lock_guard<std::mutex> lock(messageMutex_);
auto res = treatedMessages_.emplace(id);
if (res.second) {
saveTreatedMessages();
return false;
}
return true;
}
/**
* returns whether or not UPnP is enabled and active_
* ie: if it is able to make port mappings
*/
bool
ConnectionManager::Impl::getUPnPActive() const
{
return config_->getUPnPActive();
}
IpAddr
ConnectionManager::Impl::getPublishedIpAddress(uint16_t family) const
{
if (family == AF_INET)
return publishedIp_[0];
if (family == AF_INET6)
return publishedIp_[1];
assert(family == AF_UNSPEC);
// If family is not set, prefere IPv4 if available. It's more
// likely to succeed behind NAT.
if (publishedIp_[0])
return publishedIp_[0];
if (publishedIp_[1])
return publishedIp_[1];
return {};
}
void
ConnectionManager::Impl::setPublishedAddress(const IpAddr& ip_addr)
{
if (ip_addr.getFamily() == AF_INET) {
publishedIp_[0] = ip_addr;
} else {
publishedIp_[1] = ip_addr;
}
}
void
ConnectionManager::Impl::storeActiveIpAddress(std::function<void()>&& cb)
{
dht()->getPublicAddress([this, cb = std::move(cb)](std::vector<dht::SockAddr>&& results) {
bool hasIpv4 {false}, hasIpv6 {false};
for (auto& result : results) {
auto family = result.getFamily();
if (family == AF_INET) {
if (not hasIpv4) {
hasIpv4 = true;
JAMI_DBG("Store DHT public IPv4 address : %s", result.toString().c_str());
setPublishedAddress(*result.get());
if (config_->upnpCtrl_) {
config_->upnpCtrl_->setPublicAddress(*result.get());
}
}
} else if (family == AF_INET6) {
if (not hasIpv6) {
hasIpv6 = true;
JAMI_DBG("Store DHT public IPv6 address : %s", result.toString().c_str());
setPublishedAddress(*result.get());
}
}
if (hasIpv4 and hasIpv6)
break;
}
if (cb)
cb();
});
}
void
ConnectionManager::Impl::getIceOptions(std::function<void(IceTransportOptions&&)> cb) noexcept
{
storeActiveIpAddress([this, cb = std::move(cb)] {
IceTransportOptions opts = ConnectionManager::Impl::getIceOptions();
auto publishedAddr = getPublishedIpAddress();
if (publishedAddr) {
auto interfaceAddr = ip_utils::getInterfaceAddr(getLocalInterface(),
publishedAddr.getFamily());
if (interfaceAddr) {
opts.accountLocalAddr = interfaceAddr;
opts.accountPublicAddr = publishedAddr;
}
}
if (cb)
cb(std::move(opts));
});
}
IceTransportOptions
ConnectionManager::Impl::getIceOptions() const noexcept
{
IceTransportOptions opts;
opts.upnpEnable = getUPnPActive();
if (config_->stunEnabled_)
opts.stunServers.emplace_back(StunServerInfo().setUri(config_->stunServer_));
if (config_->turnEnabled_) {
auto cached = false;
std::lock_guard<std::mutex> lk(config_->cachedTurnMutex_);
cached = config_->cacheTurnV4_ || config_->cacheTurnV6_;
if (config_->cacheTurnV4_ && *config_->cacheTurnV4_) {
opts.turnServers.emplace_back(TurnServerInfo()
.setUri(config_->cacheTurnV4_->toString(true))
.setUsername(config_->turnServerUserName_)
.setPassword(config_->turnServerPwd_)
.setRealm(config_->turnServerRealm_));
}
// NOTE: first test with ipv6 turn was not concluant and resulted in multiple
// co issues. So this needs some debug. for now just disable
// if (cacheTurnV6_ && *cacheTurnV6_) {
// opts.turnServers.emplace_back(TurnServerInfo()
// .setUri(cacheTurnV6_->toString(true))
// .setUsername(turnServerUserName_)
// .setPassword(turnServerPwd_)
// .setRealm(turnServerRealm_));
//}
// Nothing cached, so do the resolution
if (!cached) {
opts.turnServers.emplace_back(TurnServerInfo()
.setUri(config_->turnServer_)
.setUsername(config_->turnServerUserName_)
.setPassword(config_->turnServerPwd_)
.setRealm(config_->turnServerRealm_));
}
}
return opts;
}
bool
ConnectionManager::Impl::foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& crt,
dht::InfoHash& account_id)
{
if (not crt)
return false;
auto top_issuer = crt;
while (top_issuer->issuer)
top_issuer = top_issuer->issuer;
// Device certificate can't be self-signed
if (top_issuer == crt) {
JAMI_WARN("Found invalid peer device: %s", crt->getLongId().toString().c_str());
return false;
}
// Check peer certificate chain
// Trust store with top issuer as the only CA
dht::crypto::TrustList peer_trust;
peer_trust.add(*top_issuer);
if (not peer_trust.verify(*crt)) {
JAMI_WARN("Found invalid peer device: %s", crt->getLongId().toString().c_str());
return false;
}
// Check cached OCSP response
if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
JAMI_ERR("Certificate %s is disabled by cached OCSP response", crt->getLongId().to_c_str());
return false;
}
account_id = crt->issuer->getId();
JAMI_WARN("Found peer device: %s account:%s CA:%s",
crt->getLongId().toString().c_str(),
account_id.toString().c_str(),
top_issuer->getId().toString().c_str());
return true;
}
bool
ConnectionManager::Impl::findCertificate(
const dht::PkId& id, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (auto cert = certStore().getCertificate(id.toString())) {
if (cb)
cb(cert);
} else if (cb)
cb(nullptr);
return true;
}
bool
ConnectionManager::Impl::findCertificate(const dht::InfoHash& h,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (auto cert = certStore().getCertificate(h.toString())) {
if (cb)
cb(cert);
} else {
dht()->findCertificate(h,
[cb = std::move(cb), this](
const std::shared_ptr<dht::crypto::Certificate>& crt) {
if (crt)
certStore().pinCertificate(crt);
if (cb)
cb(crt);
});
}
return true;
}
ConnectionManager::ConnectionManager(std::shared_ptr<ConnectionManager::Config> config_)
: pimpl_ {std::make_shared<Impl>(config_)}
{}
ConnectionManager::~ConnectionManager()
@ -1144,7 +1519,7 @@ ConnectionManager::closeConnectionsWith(const std::string& peerUri)
for (auto iter = pimpl_->infos_.begin(); iter != pimpl_->infos_.end();) {
auto const& [key, value] = *iter;
auto deviceId = key.first;
auto cert = pimpl_->account.certStore().getCertificate(deviceId.toString());
auto cert = pimpl_->certStore().getCertificate(deviceId.toString());
if (cert && cert->issuer && peerUri == cert->issuer->getId().toString()) {
connInfos.emplace_back(value);
peersDevices.emplace(deviceId);
@ -1197,6 +1572,12 @@ ConnectionManager::onConnectionReady(ConnectionReadyCallback&& cb)
pimpl_->connReadyCb_ = std::move(cb);
}
void
ConnectionManager::oniOSConnected(iOSConnectedCallback&& cb)
{
pimpl_->iOSConnectedCb_ = std::move(cb);
}
std::size_t
ConnectionManager::activeSockets() const
{
@ -1208,16 +1589,12 @@ void
ConnectionManager::monitor() const
{
std::lock_guard<std::mutex> lk(pimpl_->infosMtx_);
JAMI_DBG("ConnectionManager for account %s (%s), current status:",
pimpl_->account.getAccountID().c_str(),
pimpl_->account.getUserUri().c_str());
JAMI_DBG("ConnectionManager current status:");
for (const auto& [_, ci] : pimpl_->infos_) {
if (ci->socket_)
ci->socket_->monitor();
}
JAMI_DBG("ConnectionManager for account %s (%s), end status.",
pimpl_->account.getAccountID().c_str(),
pimpl_->account.getUserUri().c_str());
JAMI_DBG("ConnectionManager end status.");
}
void
@ -1230,4 +1607,55 @@ ConnectionManager::connectivityChanged()
}
}
void
ConnectionManager::getIceOptions(std::function<void(IceTransportOptions&&)> cb) noexcept
{
pimpl_->getIceOptions(std::move(cb));
}
IceTransportOptions
ConnectionManager::getIceOptions() const noexcept
{
return pimpl_->getIceOptions();
}
IpAddr
ConnectionManager::getPublishedIpAddress(uint16_t family) const
{
return pimpl_->getPublishedIpAddress(family);
}
void
ConnectionManager::setPublishedAddress(const IpAddr& ip_addr)
{
pimpl_->setPublishedAddress(ip_addr);
}
void
ConnectionManager::storeActiveIpAddress(std::function<void()>&& cb)
{
pimpl_->storeActiveIpAddress(std::move(cb));
}
std::shared_ptr<ConnectionManager::Config>
ConnectionManager::getConfig()
{
return pimpl_->config_;
}
bool
ConnectionManager::findCertificate(const dht::PkId& id,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
return pimpl_->findCertificate(id, std::move(cb));
}
bool
ConnectionManager::findCertificate(
const dht::InfoHash& h,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
return pimpl_->findCertificate(h, std::move(cb));
}
} // namespace jami

View File

@ -27,10 +27,11 @@
#include <opendht/default_types.h>
#include "multiplexed_socket.h"
#include "connectivity/security/diffie-hellman.h"
#include "connectivity/upnp/upnp_control.h"
namespace jami {
class JamiAccount;
class ChannelSocket;
class ConnectionManager;
@ -69,6 +70,9 @@ using ConnectCallback = std::function<void(const std::shared_ptr<ChannelSocket>&
using ConnectionReadyCallback = std::function<
void(const DeviceId&, const std::string& /* channel_name */, std::shared_ptr<ChannelSocket>)>;
using iOSConnectedCallback
= std::function<bool(const std::string& /* connType */, dht::InfoHash /* peer_h */)>;
/**
* Manages connections to other devices
* @note the account MUST be valid if ConnectionManager lives
@ -76,7 +80,9 @@ using ConnectionReadyCallback = std::function<
class ConnectionManager
{
public:
ConnectionManager(JamiAccount& account);
class Config;
ConnectionManager(std::shared_ptr<Config> config_);
~ConnectionManager();
/**
@ -145,6 +151,12 @@ public:
*/
void onConnectionReady(ConnectionReadyCallback&& cb);
/**
* Trigger cb when connection with peer is ready for iOS devices
* @param cb Callback to trigger
*/
void oniOSConnected(iOSConnectedCallback&& cb);
/**
* @return the number of active sockets
*/
@ -160,10 +172,119 @@ public:
*/
void connectivityChanged();
/**
* Create and return ICE options.
*/
void getIceOptions(std::function<void(IceTransportOptions&&)> cb) noexcept;
IceTransportOptions getIceOptions() const noexcept;
/**
* Get the published IP address, fallbacks to NAT if family is unspecified
* Prefers the usage of IPv4 if possible.
*/
IpAddr getPublishedIpAddress(uint16_t family = PF_UNSPEC) const;
/**
* Set published IP address according to given family
*/
void setPublishedAddress(const IpAddr& ip_addr);
/**
* Store the local/public addresses used to register
*/
void storeActiveIpAddress(std::function<void()>&& cb = {});
std::shared_ptr<Config> getConfig();
bool findCertificate(const dht::PkId& id,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb);
bool findCertificate(
const dht::InfoHash& h,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb = {});
private:
ConnectionManager() = delete;
class Impl;
std::shared_ptr<Impl> pimpl_;
};
class ConnectionManager::Config : public std::enable_shared_from_this<ConnectionManager::Config>
{
public:
explicit Config(std::shared_ptr<dht::DhtRunner> dht_,
const dht::crypto::Identity& id_,
tls::CertificateStore& certStore_,
std::shared_ptr<jami::upnp::Controller> upnpCtrl_,
std::string turnServer_,
std::string turnServerUserName_,
std::string turnServerPwd_,
std::string turnServerRealm_,
bool turnEnabled_)
: dht_ {std::move(dht_)}
, id_ {id_}
, upnpCtrl_ {std::move(upnpCtrl_)}
, turnServer_ {std::move(turnServer_)}
, turnServerUserName_ {std::move(turnServerUserName_)}
, turnServerPwd_ {std::move(turnServerPwd_)}
, turnServerRealm_ {std::move(turnServerRealm_)}
, turnEnabled_ {turnEnabled_}
, certStore_(&certStore_)
{}
~Config() {}
/**
* Determine if STUN public address resolution is required to register this account. In this
* case a STUN server hostname must be specified.
*/
bool stunEnabled_ {false};
/**
* The STUN server hostname (optional), used to provide the public IP address in case the
* softphone stay behind a NAT.
*/
std::string stunServer_ {};
/**
* Determine if TURN public address resolution is required to register this account. In this
* case a TURN server hostname must be specified.
*/
bool turnEnabled_ {false};
/**
* The TURN server hostname (optional), used to provide the public IP address in case the
* softphone stay behind a NAT.
*/
std::string turnServer_;
std::string turnServerUserName_;
std::string turnServerPwd_;
std::string turnServerRealm_;
mutable std::mutex cachedTurnMutex_ {};
std::unique_ptr<IpAddr> cacheTurnV4_ {};
std::unique_ptr<IpAddr> cacheTurnV6_ {};
std::string cachePath {};
std::shared_ptr<dht::DhtRunner> dht_;
const dht::crypto::Identity& id_;
const dht::crypto::Identity& identity() const { return id_; }
tls::CertificateStore* certStore_;
/**
* UPnP IGD controller and the mutex to access it
*/
bool upnpEnabled_;
mutable std::mutex upnp_mtx {};
std::shared_ptr<jami::upnp::Controller> upnpCtrl_;
/**
* returns whether or not UPnP is enabled and active
* ie: if it is able to make port mappings
*/
bool getUPnPActive() const;
};
} // namespace jami

View File

@ -20,6 +20,7 @@
#include "diffie-hellman.h"
#include "logger.h"
#include "fileutils.h"
#include <chrono>
#include <ciso646>
@ -106,5 +107,33 @@ DhParams::generate()
return params;
}
DhParams
DhParams::loadDhParams(const std::string& path)
{
std::lock_guard<std::mutex> l(fileutils::getFileLock(path));
try {
// writeTime throw exception if file doesn't exist
auto duration = std::chrono::system_clock::now() - fileutils::writeTime(path);
if (duration >= std::chrono::hours(24 * 3)) // file is valid only 3 days
throw std::runtime_error("file too old");
JAMI_DBG("Loading DhParams from file '%s'", path.c_str());
return {fileutils::loadFile(path)};
} catch (const std::exception& e) {
JAMI_DBG("Failed to load DhParams file '%s': %s", path.c_str(), e.what());
if (auto params = tls::DhParams::generate()) {
try {
fileutils::saveFile(path, params.serialize(), 0600);
JAMI_DBG("Saved DhParams to file '%s'", path.c_str());
} catch (const std::exception& ex) {
JAMI_WARN("Failed to save DhParams in file '%s': %s", path.c_str(), ex.what());
}
return params;
}
JAMI_ERR("Can't generate DH params.");
return {};
}
}
} // namespace tls
} // namespace jami

View File

@ -25,6 +25,7 @@
#include <vector>
#include <memory>
#include <cstdint>
#include <string>
namespace jami {
namespace tls {
@ -60,6 +61,8 @@ public:
static DhParams generate();
static DhParams loadDhParams(const std::string& path);
private:
std::unique_ptr<gnutls_dh_params_int, decltype(gnutls_dh_params_deinit)*>
params_ {nullptr, gnutls_dh_params_deinit};

View File

@ -381,7 +381,7 @@ JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami::
return {};
auto uri = Uri(toUrl);
getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) {
connectionManager_->getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) {
if (call->isIceEnabled()) {
if (not call->createIceMediaTransport(false)
or not call->initIceMediaTransport(true, std::forward<IceTransportOptions>(opts))) {
@ -752,8 +752,9 @@ JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call,
const auto localAddress = ip_utils::getInterfaceAddr(getLocalInterface(), target.getFamily());
IpAddr addrSdp = getPublishedSameasLocal() ? localAddress
: getPublishedIpAddress(target.getFamily());
IpAddr addrSdp = getPublishedSameasLocal()
? localAddress
: connectionManager_->getPublishedIpAddress(target.getFamily());
// fallback on local address
if (not addrSdp)
@ -1928,7 +1929,7 @@ JamiAccount::doRegister_()
connectionManager_->onICERequest([this](const DeviceId& deviceId) {
std::promise<bool> accept;
std::future<bool> fut = accept.get_future();
accountManager_->findCertificate(
connectionManager_->findCertificate(
deviceId, [this, &accept](const std::shared_ptr<dht::crypto::Certificate>& cert) {
dht::InfoHash peer_account_id;
auto res = accountManager_->onPeerCertificate(cert,
@ -2336,7 +2337,8 @@ JamiAccount::setRegistrationState(RegistrationState state,
if (state == RegistrationState::REGISTERED) {
JAMI_WARNING("[Account {}] connected", getAccountID());
turnCache_->refresh();
storeActiveIpAddress();
if (connectionManager_)
connectionManager_->storeActiveIpAddress();
} else if (state == RegistrationState::TRYING) {
JAMI_WARNING("[Account {}] connecting…", getAccountID());
} else {
@ -2362,11 +2364,12 @@ JamiAccount::connectivityChanged()
dht_->connectivityChanged();
{
std::lock_guard<std::mutex> lkCM(connManagerMtx_);
if (connectionManager_)
if (connectionManager_) {
connectionManager_->connectivityChanged();
// reset cache
connectionManager_->setPublishedAddress({});
}
}
// reset cache
setPublishedAddress({});
}
bool
@ -2374,8 +2377,8 @@ JamiAccount::findCertificate(
const dht::InfoHash& h,
std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (accountManager_)
return accountManager_->findCertificate(h, std::move(cb));
if (connectionManager_)
return connectionManager_->findCertificate(h, std::move(cb));
return false;
}
@ -2383,16 +2386,16 @@ bool
JamiAccount::findCertificate(
const dht::PkId& id, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
{
if (accountManager_)
return accountManager_->findCertificate(id, std::move(cb));
if (connectionManager_)
return connectionManager_->findCertificate(id, std::move(cb));
return false;
}
bool
JamiAccount::findCertificate(const std::string& crt_id)
{
if (accountManager_)
return accountManager_->findCertificate(dht::InfoHash(crt_id));
if (connectionManager_)
return connectionManager_->findCertificate(dht::InfoHash(crt_id));
return false;
}
@ -2525,34 +2528,6 @@ JamiAccount::getKnownDevices() const
return ids;
}
tls::DhParams
JamiAccount::loadDhParams(std::string path)
{
std::lock_guard<std::mutex> l(fileutils::getFileLock(path));
try {
// writeTime throw exception if file doesn't exist
auto duration = clock::now() - fileutils::writeTime(path);
if (duration >= std::chrono::hours(24 * 3)) // file is valid only 3 days
throw std::runtime_error("file too old");
JAMI_DBG("Loading DhParams from file '%s'", path.c_str());
return {fileutils::loadFile(path)};
} catch (const std::exception& e) {
JAMI_DBG("Failed to load DhParams file '%s': %s", path.c_str(), e.what());
if (auto params = tls::DhParams::generate()) {
try {
fileutils::saveFile(path, params.serialize(), 0600);
JAMI_DBG("Saved DhParams to file '%s'", path.c_str());
} catch (const std::exception& ex) {
JAMI_WARN("Failed to save DhParams in file '%s': %s", path.c_str(), ex.what());
}
return params;
}
JAMI_ERR("Can't generate DH params.");
return {};
}
}
void
JamiAccount::loadCachedUrl(const std::string& url,
const std::string& cachePath,
@ -2687,7 +2662,7 @@ JamiAccount::generateDhParams()
// make sure cachePath_ is writable
fileutils::check_dir(cachePath_.c_str(), 0700);
dhParams_ = dht::ThreadPool::computation().get<tls::DhParams>(
std::bind(loadDhParams, cachePath_ + DIR_SEPARATOR_STR "dhParams"));
std::bind(tls::DhParams::loadDhParams, cachePath_ + DIR_SEPARATOR_STR "dhParams"));
}
MatchRank
@ -3332,59 +3307,10 @@ JamiAccount::onIsComposing(const std::string& conversationId,
}
}
void
JamiAccount::getIceOptions(std::function<void(IceTransportOptions&&)> cb) noexcept
IceTransportOptions
JamiAccount::getIceOptions() const noexcept
{
storeActiveIpAddress([this, cb = std::move(cb)] {
auto opts = SIPAccountBase::getIceOptions();
auto publishedAddr = getPublishedIpAddress();
if (publishedAddr) {
auto interfaceAddr = ip_utils::getInterfaceAddr(getLocalInterface(),
publishedAddr.getFamily());
if (interfaceAddr) {
opts.accountLocalAddr = interfaceAddr;
opts.accountPublicAddr = publishedAddr;
}
}
if (cb)
cb(std::move(opts));
});
}
void
JamiAccount::storeActiveIpAddress(std::function<void()>&& cb)
{
dht_->getPublicAddress([this, cb = std::move(cb)](std::vector<dht::SockAddr>&& results) {
bool hasIpv4 {false}, hasIpv6 {false};
for (auto& result : results) {
auto family = result.getFamily();
if (family == AF_INET) {
if (not hasIpv4) {
hasIpv4 = true;
JAMI_DBG("[Account %s] Store DHT public IPv4 address : %s",
getAccountID().c_str(),
result.toString().c_str());
setPublishedAddress(*result.get());
if (upnpCtrl_) {
upnpCtrl_->setPublicAddress(*result.get());
}
}
} else if (family == AF_INET6) {
if (not hasIpv6) {
hasIpv6 = true;
JAMI_DBG("[Account %s] Store DHT public IPv6 address : %s",
getAccountID().c_str(),
result.toString().c_str());
setPublishedAddress(*result.get());
}
}
if (hasIpv4 and hasIpv6)
break;
}
if (cb)
cb();
});
return connectionManager_->getIceOptions();
}
bool
@ -4255,15 +4181,38 @@ void
JamiAccount::initConnectionManager()
{
if (!connectionManager_) {
connectionManager_ = std::make_unique<ConnectionManager>(*this);
channelHandlers_[Uri::Scheme::SWARM]
= std::make_unique<SwarmChannelHandler>(shared(), *connectionManager_.get());
auto connectionManagerConfig
= std::make_shared<ConnectionManager::Config>(dht(),
identity(),
certStore(),
upnpCtrl_,
config().turnServer,
config().turnServerUserName,
config().turnServerPwd,
config().turnServerRealm,
config().turnEnabled);
connectionManagerConfig->cachePath = cachePath_;
connectionManager_ = std::make_unique<ConnectionManager>(connectionManagerConfig);
channelHandlers_[Uri::Scheme::GIT]
= std::make_unique<ConversationChannelHandler>(shared(), *connectionManager_.get());
channelHandlers_[Uri::Scheme::SYNC]
= std::make_unique<SyncChannelHandler>(shared(), *connectionManager_.get());
channelHandlers_[Uri::Scheme::DATA_TRANSFER]
= std::make_unique<TransferChannelHandler>(shared(), *connectionManager_.get());
#if TARGET_OS_IOS
connectionManager_->oniOSConnected([&](const std::string& connType, dht::InfoHash peer_h) {
if ((connType == "videoCall" || connType == "audioCall")
&& jami::Manager::instance().isIOSExtension) {
bool hasVideo = connType == "videoCall";
emitSignal<libjami::ConversationSignal::CallConnectionRequest>("",
peer_h.toString(),
hasVideo);
return true;
}
return false;
});
#endif
}
}

View File

@ -329,6 +329,11 @@ public:
const std::map<std::string, std::string>& msg);
void onIsComposing(const std::string& conversationId, const std::string& peer, bool isWriting);
/**
* Create and return ICE options.
*/
IceTransportOptions getIceOptions() const noexcept;
/* Devices */
void addDevice(const std::string& password);
/**
@ -423,16 +428,6 @@ public:
*/
std::map<std::string, std::string> getNearbyPeers() const override;
/**
* Store the local/public addresses used to register
*/
void storeActiveIpAddress(std::function<void()>&& cb = {});
/**
* Create and return ICE options.
*/
void getIceOptions(std::function<void(IceTransportOptions&&)> cb) noexcept;
#ifdef LIBJAMI_TESTABLE
ConnectionManager& connectionManager()
{
@ -693,8 +688,6 @@ private:
const std::shared_ptr<dht::crypto::Certificate>& from_cert,
const dht::InfoHash& from);
static tls::DhParams loadDhParams(std::string path);
void loadCachedUrl(const std::string& url,
const std::string& cachePath,
const std::chrono::seconds& cacheDuration,
@ -792,7 +785,6 @@ private:
*/
// TODO move in separate class
void testTurn(IpAddr server);
void cacheTurnServers();
std::unique_ptr<TurnTransport> testTurnV4_;
std::unique_ptr<TurnTransport> testTurnV6_;
void refreshTurnDelay(bool scheduleNext);

View File

@ -411,7 +411,6 @@ transaction_request_cb(pjsip_rx_data* rdata)
auto call = account->newIncomingCall(std::string(remote_user),
MediaAttribute::mediaAttributesToMediaMaps(localMediaList),
transport);
if (!call) {
return PJ_FALSE;
}

View File

@ -297,9 +297,10 @@ CallTest::testDeclineMultiDevice()
JAMI_INFO("Start call between alice and Bob");
auto bobAccount2 = Manager::instance().getAccount<JamiAccount>(bob2Id);
auto call = libjami::placeCallWithMedia(aliceId, bobUri, {});
CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived == 2 && !callIdBob.empty(); }));
CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return callReceived == 2 && !callIdBob.empty(); }));
JAMI_INFO("Stop call between alice and Bob");
callStopped = 0;