diff --git a/CMakeLists.txt b/CMakeLists.txt index 1aab0947..268a3841 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,7 @@ include(policyRules) set(CMAKE_AUTOMOC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 20) # Detect MXE cross-compilation set(IRIS_DEFAULT_BUNDLED_USRSCTP OFF) @@ -61,15 +61,14 @@ endif() option(IRIS_ENABLE_INSTALL "Enable installation" ON) option(IRIS_ENABLE_JINGLE_SCTP "Enable SCTP over ICE Jingle transport / data channels" ON) -option(IRIS_BUNDLED_QCA "Adds: DTLS, Blake2b and other useful for XMPP crypto-stuff" ${IRIS_DEFAULT_BUNDLED_QCA}) +# note Blake2b is needed only with Qt5. Qt6 has its own implementation +option(IRIS_BUNDLED_QCA "Adds: DTLS, Blake2b (needed with Qt5) and other useful for XMPP crypto-stuff" ${IRIS_DEFAULT_BUNDLED_QCA}) option(IRIS_BUNDLED_USRSCTP "Compile compatible UsrSCTP lib (required for datachannel Jingle transport)" ${IRIS_DEFAULT_BUNDLED_USRSCTP}) option(IRIS_BUILD_TOOLS "Build tools and examples" OFF) option(IRIS_ENABLE_DEBUG "Enable debugging code paths" OFF) set(IRIS_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}/xmpp/iris) -set(CMAKE_CXX_STANDARD 17) - if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")) include(debug-definitions) endif() @@ -101,7 +100,7 @@ if(IRIS_ENABLE_JINGLE_SCTP) include(IrisSCTP) endif() -if(NOT IRIS_BUNDLED_QCA) +if(NOT IRIS_BUNDLED_QCA AND QT_DEFAULT_MAJOR_VERSION LESS 6) find_package(B2 QUIET) if(B2_FOUND) message(STATUS "Found B2: ${B2_LIBRARY}") diff --git a/cmake/modules/IrisQCA.cmake b/cmake/modules/IrisQCA.cmake index e43b80fd..c6e0efce 100644 --- a/cmake/modules/IrisQCA.cmake +++ b/cmake/modules/IrisQCA.cmake @@ -68,6 +68,7 @@ if(IRIS_BUNDLED_QCA) CMAKE_ARGS ${QCA_BUILD_OPTIONS} BUILD_BYPRODUCTS ${Qca_LIBRARY} INSTALL_COMMAND "" + UPDATE_COMMAND "" ) endif() else() diff --git a/cmake/modules/IrisSCTP.cmake b/cmake/modules/IrisSCTP.cmake index 42226968..5a9bdca8 100644 --- a/cmake/modules/IrisSCTP.cmake +++ b/cmake/modules/IrisSCTP.cmake @@ -37,9 +37,13 @@ else() -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}) - if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - list(APPEND USRSCTP_BUILD_OPTIONS -DCMAKE_C_FLAGS="-Wno-maybe-uninitialized") - endif() + # Setting these options seems to have no any effect because those prepended and not appended + # if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # list(APPEND USRSCTP_BUILD_OPTIONS "-DCMAKE_C_FLAGS=-Wno-maybe-uninitialized") + # endif() + # if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # list(APPEND USRSCTP_BUILD_OPTIONS "-DCMAKE_C_FLAGS=-Wno-uninitialized -Wno-unused-but-set-variable") + # endif() if (EXISTS ${USRSCTP_SOURCE_DIR}) message(STATUS "USRSCTP: found bundled sources") ExternalProject_Add(UsrSCTPProject @@ -56,14 +60,20 @@ else() if(NOT Git_FOUND) message(FATAL_ERROR "Git not found! Bundled UsrSCTP needs Git utility.\nPlease set GIT_EXECUTABLE variable or add git to PATH") endif() + set(patch_command + ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/usrsctp.patch && + ${GIT_EXECUTABLE} checkout /usrsctplib/netinet/sctp_output.c && + ${GIT_EXECUTABLE} apply /usrsctp.patch) ExternalProject_Add(UsrSCTPProject PREFIX ${USRSCTP_PREFIX} BINARY_DIR ${USRSCTP_BUILD_DIR} GIT_REPOSITORY ${IrisSCTPGitRepo} - GIT_TAG a17109528c75d01f6372d5c30851a639684c6e99 + GIT_TAG 848eca82f92273af9a79687a90343a2ebcf3481d CMAKE_ARGS ${USRSCTP_BUILD_OPTIONS} BUILD_BYPRODUCTS ${USRSCTP_LIBRARY} INSTALL_COMMAND "" + PATCH_COMMAND ${patch_command} + UPDATE_COMMAND "" ) endif() add_library(SctpLab::UsrSCTP UNKNOWN IMPORTED) diff --git a/cmake/modules/usrsctp.patch b/cmake/modules/usrsctp.patch new file mode 100644 index 00000000..25a6d239 --- /dev/null +++ b/cmake/modules/usrsctp.patch @@ -0,0 +1,19 @@ + + +diff --git a/usrsctplib/netinet/sctp_output.c b/usrsctplib/netinet/sctp_output.c +index b8a7b46..6f9c9b9 100644 +--- a/usrsctplib/netinet/sctp_output.c ++++ b/usrsctplib/netinet/sctp_output.c +@@ -13562,10 +13562,10 @@ sctp_lower_sosend(struct socket *so, + #endif + struct timeval now; + struct sctp_block_entry be; +- struct sctp_inpcb *inp; ++ struct sctp_inpcb *inp = NULL; + struct sctp_tcb *stcb = NULL; + struct sctp_nets *net; +- struct sctp_association *asoc; ++ struct sctp_association *asoc = NULL; + struct sctp_inpcb *t_inp; + struct sctp_nonpad_sndrcvinfo *sndrcvninfo; + ssize_t sndlen = 0, max_len, local_add_more; diff --git a/include/iris/xmpp_vcard4.h b/include/iris/xmpp_vcard4.h new file mode 100644 index 00000000..c2c788e7 --- /dev/null +++ b/include/iris/xmpp_vcard4.h @@ -0,0 +1 @@ +#include "xmpp/xmpp-im/xmpp_vcard4.h" diff --git a/src/irisnet/corelib/netinterface_qtnet.cpp b/src/irisnet/corelib/netinterface_qtnet.cpp index 2a303ad8..52e7399f 100644 --- a/src/irisnet/corelib/netinterface_qtnet.cpp +++ b/src/irisnet/corelib/netinterface_qtnet.cpp @@ -51,7 +51,7 @@ class InterfaceMonitor : public QObject { notifier = new QSocketNotifier(netlinkFd, QSocketNotifier::Read, this); connect(notifier, &QSocketNotifier::activated, this, - [=](QSocketDescriptor, QSocketNotifier::Type) { emit changed(); }); + [this](QSocketDescriptor, QSocketNotifier::Type) { emit changed(); }); } ~InterfaceMonitor() diff --git a/src/irisnet/corelib/netnames.cpp b/src/irisnet/corelib/netnames.cpp index 64be6fbe..6e48666e 100644 --- a/src/irisnet/corelib/netnames.cpp +++ b/src/irisnet/corelib/netnames.cpp @@ -19,7 +19,7 @@ #include "netnames.h" -#include "addressresolver.h" +// #include "addressresolver.h" #include "corelib/irisnetglobal_p.h" #include "irisnetplugin.h" @@ -27,11 +27,21 @@ #include #endif #include +#include -// #define NETNAMES_DEBUG -#ifdef NETNAMES_DEBUG #define NNDEBUG (qDebug() << this << "#" << __FUNCTION__ << ":") -#endif +static std::optional enable_logs; +#define NNLOG(msg) \ + { \ + if (!enable_logs.has_value()) { \ + enable_logs = qgetenv("NN_DEBUG") == "1"; \ + } \ + if (*enable_logs) { \ + msg; \ + } \ + } \ + while (false) \ + ; namespace XMPP { //---------------------------------------------------------------------------- @@ -308,8 +318,8 @@ QDebug operator<<(QDebug dbg, XMPP::NameRecord::Type type) QDebug operator<<(QDebug dbg, const XMPP::NameRecord &record) { - dbg.nospace() << "XMPP::NameRecord(" << "owner=" << record.owner() << ", ttl=" << record.ttl() - << ", type=" << record.type(); + dbg.nospace() << "XMPP::NameRecord(" + << "owner=" << record.owner() << ", ttl=" << record.ttl() << ", type=" << record.type(); switch (record.type()) { case XMPP::NameRecord::A: @@ -362,7 +372,8 @@ class ServiceInstance::Private : public QSharedData { ServiceInstance::ServiceInstance() : d(new Private) { } ServiceInstance::ServiceInstance(const QString &instance, const QString &type, const QString &domain, - const QMap &attribs) : d(new Private) + const QMap &attribs) : + d(new Private) { d->instance = instance; d->type = type; @@ -444,7 +455,7 @@ class ServiceResolver::Private : public QObject { QAbstractSocket::NetworkLayerProtocol protocol; //!< IP protocol we are currently looking up XMPP::WeightedNameRecordList srvList; //!< List of resolved SRV names - QList hostList; //!< List or resolved hostnames for current SRV name + QList hostList; //!< List or resolved hostnames for current SRV name QList resolverList; //!< NameResolvers currently in use, needed for cleanup }; @@ -452,7 +463,7 @@ WeightedNameRecordList::WeightedNameRecordList() : currentPriorityGroup(priority { } -WeightedNameRecordList::WeightedNameRecordList(const QList &list) { append(list); } +WeightedNameRecordList::WeightedNameRecordList(const QList &list) { append(list); } WeightedNameRecordList::WeightedNameRecordList(const WeightedNameRecordList &other) { *this = other; } @@ -474,7 +485,7 @@ bool WeightedNameRecordList::isEmpty() const return currentPriorityGroup == const_cast(this)->priorityGroups.end(); } -XMPP::NameRecord WeightedNameRecordList::takeNext() +ServiceBoundRecord WeightedNameRecordList::takeNext() { /* Find the next useful priority group */ while (currentPriorityGroup != priorityGroups.end() && currentPriorityGroup->second.empty()) { @@ -482,47 +493,36 @@ XMPP::NameRecord WeightedNameRecordList::takeNext() } /* There are no priority groups left, return failure */ if (currentPriorityGroup == priorityGroups.end()) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "No more SRV records left"; -#endif - return XMPP::NameRecord(); + NNLOG(NNDEBUG << "No more SRV records left"); + return {}; } /* Find the new total weight of this priority group */ int totalWeight = 0; for (const auto &record : std::as_const(currentPriorityGroup->second)) { - totalWeight += record.weight(); + totalWeight += record.record.weight(); } -#ifdef NETNAMES_DEBUG - NNDEBUG << "Total weight:" << totalWeight; -#endif + NNLOG(NNDEBUG << "Total weight:" << totalWeight); /* Pick a random entry */ -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) int randomWeight = totalWeight ? QRandomGenerator::global()->bounded(totalWeight) : 0; -#else - int randomWeight = qrand() / static_cast(RAND_MAX) * totalWeight; -#endif -#ifdef NETNAMES_DEBUG - NNDEBUG << "Picked weight:" << randomWeight; -#endif + NNLOG(NNDEBUG << "Picked weight:" << randomWeight); /* Iterate through the priority group until we found the randomly selected entry */ WeightedNameRecordPriorityGroup::iterator it(currentPriorityGroup->second.begin()); - for (int currentWeight = it->weight(); currentWeight < randomWeight; currentWeight += (++it)->weight()) { } + for (int currentWeight = it->record.weight(); currentWeight < randomWeight; + currentWeight += (++it)->record.weight()) { } Q_ASSERT(it != currentPriorityGroup->second.end()); /* We are going to delete the entry in the list, so save it */ - XMPP::NameRecord result(*it); + auto result { *it }; -#ifdef NETNAMES_DEBUG - NNDEBUG << "Picked record:" << result; -#endif + NNLOG(NNDEBUG << "Picked record:" << result); /* Delete the entry from list, to prevent it from being tried multiple times */ - currentPriorityGroup->second.remove(it->weight(), *it); + currentPriorityGroup->second.remove(it->record.weight(), *it); if (currentPriorityGroup->second.isEmpty()) { currentPriorityGroup = priorityGroups.erase(currentPriorityGroup); } @@ -542,7 +542,7 @@ void WeightedNameRecordList::append(const XMPP::WeightedNameRecordList &list) { /* Copy over all records from all groups */ for (const auto &group : list.priorityGroups) { - for (const NameRecord &record : group.second) + for (const auto &record : group.second) append(record); } @@ -550,21 +550,21 @@ void WeightedNameRecordList::append(const XMPP::WeightedNameRecordList &list) currentPriorityGroup = priorityGroups.begin(); } -void WeightedNameRecordList::append(const QList &list) +void WeightedNameRecordList::append(const QList &list) { - for (const XMPP::NameRecord &record : list) - if (record.type() == XMPP::NameRecord::Srv) + for (const auto &record : list) + if (record.record.type() == XMPP::NameRecord::Srv) append(record); /* Reset to beginning */ currentPriorityGroup = priorityGroups.begin(); } -void WeightedNameRecordList::append(const XMPP::NameRecord &record) +void WeightedNameRecordList::append(const ServiceBoundRecord &record) { - Q_ASSERT(record.type() == XMPP::NameRecord::Srv); - auto [it, _] = priorityGroups.try_emplace(record.priority(), WeightedNameRecordPriorityGroup {}); - it->second.insert(record.weight(), record); + Q_ASSERT(record.record.type() == XMPP::NameRecord::Srv); + auto [it, _] = priorityGroups.try_emplace(record.record.priority(), WeightedNameRecordPriorityGroup {}); + it->second.insert(record.record.weight(), record); /* Reset to beginning */ currentPriorityGroup = priorityGroups.begin(); @@ -575,7 +575,7 @@ void WeightedNameRecordList::append(const QString &hostname, quint16 port) NameRecord record(hostname.toLocal8Bit(), std::numeric_limits::max()); record.setSrv(hostname.toLocal8Bit(), port, std::numeric_limits::max(), 0); - append(record); + append(ServiceBoundRecord { {}, record }); /* Reset to beginning */ currentPriorityGroup = priorityGroups.begin(); @@ -587,18 +587,37 @@ XMPP::WeightedNameRecordList &WeightedNameRecordList::operator<<(const XMPP::Wei return *this; } -WeightedNameRecordList &WeightedNameRecordList::operator<<(const QList &list) +WeightedNameRecordList &WeightedNameRecordList::operator<<(const QList &list) { append(list); return *this; } -XMPP::WeightedNameRecordList &WeightedNameRecordList::operator<<(const XMPP::NameRecord &record) +XMPP::WeightedNameRecordList &WeightedNameRecordList::operator<<(const ServiceBoundRecord &record) { append(record); return *this; } +QDebug operator<<(QDebug dbg, const ServiceBoundRecord &r) +{ + dbg.nospace() << "XMPP::ServiceBoundRecor(\n"; + dbg.nospace() << "service=" << r.service +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + << Qt::endl; +#else + << endl; +#endif + dbg.nospace() << "record=" << r.record +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + << Qt::endl; +#else + << endl; +#endif + dbg.nospace() << "})"; + return dbg; +} + QDebug operator<<(QDebug dbg, const XMPP::WeightedNameRecordList &list) { dbg.nospace() << "XMPP::WeightedNameRecordList(\n"; @@ -810,7 +829,7 @@ class NameManager : public QObject { p_serv, &ServiceProvider::resolve_resultsReady, this, [this](int id, const QList &results) { ServiceResolver::Private *np = sres_instances.value(id); - emit np->q->resultReady(results[0].address, quint16(results[0].port), results[0].hostName); + emit np->q->resultReady(results[0].address, quint16(results[0].port), results[0].hostName, {}); }, Qt::QueuedConnection); } @@ -1077,9 +1096,7 @@ void ServiceBrowser::stop() { } //---------------------------------------------------------------------------- ServiceResolver::ServiceResolver(QObject *parent) : QObject(parent) { -#ifdef NETNAMES_DEBUG - NNDEBUG; -#endif + NNLOG(NNDEBUG); d = new Private(this); } @@ -1088,9 +1105,7 @@ ServiceResolver::~ServiceResolver() { delete d; } void ServiceResolver::clear_resolvers() { -#ifdef NETNAMES_DEBUG - NNDEBUG; -#endif + NNLOG(NNDEBUG); /* cleanup all resolvers */ for (XMPP::NameResolver *resolver : std::as_const(d->resolverList)) { @@ -1100,9 +1115,7 @@ void ServiceResolver::clear_resolvers() void ServiceResolver::cleanup_resolver(XMPP::NameResolver *resolver) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "r:" << resolver; -#endif + NNLOG(NNDEBUG << "r:" << resolver); if (resolver) { /* @@ -1126,11 +1139,9 @@ void ServiceResolver::setProtocol(ServiceResolver::Protocol p) { d->requestedPro void ServiceResolver::start(const QByteArray &name) { NameManager::instance()->resolve_instance_start(d, name); } /* normal host lookup */ -void ServiceResolver::start(const QString &host, quint16 port) +void ServiceResolver::start(const QString &host, quint16 port, const QString &service) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "h:" << host << "p:" << port; -#endif + NNLOG(NNDEBUG << "h:" << host << "p:" << port); /* clear host list */ d->hostList.clear(); @@ -1140,16 +1151,14 @@ void ServiceResolver::start(const QString &host, quint16 port) d->host = host; d->port = port; -#ifdef NETNAMES_DEBUG - NNDEBUG << "d->p:" << d->protocol; -#endif + NNLOG(NNDEBUG << "d->p:" << d->protocol); /* initiate the host lookup */ XMPP::NameRecord::Type querytype = (d->protocol == QAbstractSocket::IPv6Protocol ? XMPP::NameRecord::Aaaa : XMPP::NameRecord::A); XMPP::NameResolver *resolver = new XMPP::NameResolver; - connect(resolver, SIGNAL(resultsReady(QList)), this, - SLOT(handle_host_ready(QList))); + connect(resolver, &XMPP::NameResolver::resultsReady, this, + [this, service](const QList records) { handle_host_ready(service, records); }); connect(resolver, SIGNAL(error(XMPP::NameResolver::Error)), this, SLOT(handle_host_error(XMPP::NameResolver::Error))); resolver->start(host.toLocal8Bit(), querytype); @@ -1157,17 +1166,11 @@ void ServiceResolver::start(const QString &host, quint16 port) } /* SRV lookup */ -void ServiceResolver::start(const QString &service, const QString &transport, const QString &domain, int port) +void ServiceResolver::start(const QStringList &services, const QString &transport, const QString &domain, int port) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "s:" << service << "t:" << transport << "d:" << domain << "p:" << port; -#endif - - QString srv_request("_" + service + "._" + transport + "." + domain + "."); - + NNLOG(NNDEBUG << "s:" << services << "t:" << transport << "d:" << domain << "p:" << port); /* clear SRV list */ d->srvList.clear(); - d->domain = domain; /* after we tried all SRV hosts, we shall connect directly (if requested) */ @@ -1178,74 +1181,82 @@ void ServiceResolver::start(const QString &service, const QString &transport, co Q_ASSERT(port == std::numeric_limits::max()); } - /* initiate the SRV lookup */ - XMPP::NameResolver *resolver = new XMPP::NameResolver; - connect(resolver, SIGNAL(resultsReady(QList)), this, - SLOT(handle_srv_ready(QList))); - connect(resolver, SIGNAL(error(XMPP::NameResolver::Error)), this, - SLOT(handle_srv_error(XMPP::NameResolver::Error))); - resolver->start(srv_request.toLocal8Bit(), XMPP::NameRecord::Srv); - d->resolverList << resolver; -} - -/* SRV request resolved, now try to connect to the hosts */ -void ServiceResolver::handle_srv_ready(const QList &r) -{ -#ifdef NETNAMES_DEBUG - NNDEBUG << "sl:" << r; -#endif - - /* cleanup resolver */ - cleanup_resolver(static_cast(sender())); - - /* lookup srv pointers */ - d->srvList << r; - emit srvReady(); - if (d->requestedProtocol != HappyEyeballs) { - try_next_srv(); - } -} - -/* failed the srv lookup, but we might have a fallback host in the srvList */ -void ServiceResolver::handle_srv_error(XMPP::NameResolver::Error e) -{ -#ifdef NETNAMES_DEBUG - NNDEBUG << "e:" << e; -#else - Q_UNUSED(e) -#endif - - /* cleanup resolver */ - cleanup_resolver(static_cast(sender())); - - /* srvList already contains a failsafe host, try that */ - emit srvFailed(); - if (d->requestedProtocol != HappyEyeballs) { - try_next_srv(); + struct SrvStats { + std::function callback; + int counter = 0; + int success = 0; + SrvStats(std::function &&cb, int cnt) : callback(std::move(cb)), counter(cnt) { } + void finishOne(bool success) + { + if (success) + this->success++; + if (--counter == 0) + callback(this->success > 0); + } + }; + + auto stats = std::make_shared( + [this](bool success) { + if (success) { + NNLOG(NNDEBUG << "emit srvReady"); + emit srvReady(); + } else { + /* srvList already contains a failsafe host, try that */ + NNLOG(NNDEBUG << "emit srvFailed"); + emit srvFailed(); + } + if (d->requestedProtocol != HappyEyeballs) { + try_next_srv(); + } + }, + services.size()); + + for (auto const &service : services) { + QString srv_request("_" + service + "._" + transport + "." + domain + "."); + + /* initiate the SRV lookup */ + auto resolver = new XMPP::NameResolver; + connect(resolver, &XMPP::NameResolver::resultsReady, this, + [this, resolver, stats, service](const QList &r) { + NNLOG(NNDEBUG << "sl:" << r); + QList sbr; + std::transform(r.begin(), r.end(), std::back_inserter(sbr), [service](auto const &r) { + return ServiceBoundRecord { service, r }; + }); + d->srvList << sbr; + stats->finishOne(true); + cleanup_resolver(resolver); + }); + connect(resolver, &XMPP::NameResolver::error, this, [this, resolver, stats](XMPP::NameResolver::Error e) { + /* failed the srv lookup, but we might have a fallback host in the srvList */ + NNLOG(NNDEBUG << "e:" << e); + stats->finishOne(false); + cleanup_resolver(resolver); + }); + resolver->start(srv_request.toLocal8Bit(), XMPP::NameRecord::Srv); + d->resolverList << resolver; } } /* hosts resolved, now try to connect to them */ -void ServiceResolver::handle_host_ready(const QList &r) +void ServiceResolver::handle_host_ready(const QString &service, const QList &rl) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "hl:" << r; -#endif + NNLOG(NNDEBUG << "hl:" << rl); /* cleanup resolver */ cleanup_resolver(static_cast(sender())); /* connect to host */ - d->hostList << r; + for (auto const &r : rl) { + d->hostList << ServiceBoundRecord { service, r }; + } try_next_host(); } /* failed to lookup the primary record (A or AAAA, depending on user choice) */ void ServiceResolver::handle_host_error(XMPP::NameResolver::Error e) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "e:" << e; -#endif + NNLOG(NNDEBUG << "e:" << e); /* cleanup resolver */ cleanup_resolver(static_cast(sender())); @@ -1260,11 +1271,7 @@ void ServiceResolver::handle_host_error(XMPP::NameResolver::Error e) /* failed to lookup the fallback record (A or AAAA, depending on user choice) */ void ServiceResolver::handle_host_fallback_error(XMPP::NameResolver::Error e) { -#ifdef NETNAMES_DEBUG - NNDEBUG << "e:" << e; -#else - Q_UNUSED(e) -#endif + NNLOG(NNDEBUG << "e:" << e); /* cleanup resolver */ cleanup_resolver(static_cast(sender())); @@ -1283,9 +1290,7 @@ bool ServiceResolver::check_protocol_fallback() /* lookup the fallback host */ bool ServiceResolver::lookup_host_fallback() { -#ifdef NETNAMES_DEBUG - NNDEBUG; -#endif + NNLOG(NNDEBUG); /* if a fallback is desired, otherwise we must fail immediately */ if (!check_protocol_fallback()) { @@ -1295,9 +1300,7 @@ bool ServiceResolver::lookup_host_fallback() d->protocol = (d->protocol == QAbstractSocket::IPv6Protocol ? QAbstractSocket::IPv4Protocol : QAbstractSocket::IPv6Protocol); -#ifdef NETNAMES_DEBUG - NNDEBUG << "d->p:" << d->protocol; -#endif + NNLOG(NNDEBUG << "d->p:" << d->protocol); /* initiate the fallback host lookup */ XMPP::NameRecord::Type querytype @@ -1316,15 +1319,13 @@ bool ServiceResolver::lookup_host_fallback() /* notify user about next host */ bool ServiceResolver::try_next_host() { -#ifdef NETNAMES_DEBUG - NNDEBUG << "hl:" << d->hostList; -#endif + NNLOG(NNDEBUG << "hl:" << d->hostList); /* if there is a host left for current protocol (AAAA or A) */ if (!d->hostList.empty()) { - XMPP::NameRecord record(d->hostList.takeFirst()); + auto record { d->hostList.takeFirst() }; /* emit found address and the port specified earlier */ - emit resultReady(record.address(), d->port, record.owner()); + emit resultReady(record.record.address(), d->port, record.record.owner(), record.service); return true; } @@ -1335,19 +1336,15 @@ bool ServiceResolver::try_next_host() /* lookup the next SRV record in line */ void ServiceResolver::try_next_srv() { -#ifdef NETNAMES_DEBUG - NNDEBUG << "sl:" << d->srvList; -#endif + NNLOG(NNDEBUG << "sl:" << d->srvList); /* if there are still hosts we did not try */ - XMPP::NameRecord record = d->srvList.takeNext(); - if (!record.isNull()) { + auto record = d->srvList.takeNext(); + if (!record.record.isNull()) { /* lookup host by name and specify port for later use */ - start(record.name(), quint16(record.port())); + start(record.record.name(), quint16(record.record.port()), record.service); } else { -#ifdef NETNAMES_DEBUG - NNDEBUG << "SRV list empty, failing"; -#endif + NNLOG(NNDEBUG << "SRV list empty, failing"); /* no more SRV hosts to try, fail */ emit error(NoHostLeft); } @@ -1374,11 +1371,15 @@ ServiceResolver::ProtoSplit ServiceResolver::happySplit() s.ipv4->d->srvList = d->srvList; s.ipv4->d->hostList = d->hostList; s.ipv4->d->domain = d->domain; + s.ipv4->d->host = d->host; + s.ipv4->d->port = d->port; s.ipv6 = new ServiceResolver(this); s.ipv6->setProtocol(IPv6); s.ipv6->d->srvList = d->srvList; s.ipv6->d->hostList = d->hostList; s.ipv6->d->domain = d->domain; + s.ipv4->d->host = d->host; + s.ipv4->d->port = d->port; return s; } diff --git a/src/irisnet/corelib/netnames.h b/src/irisnet/corelib/netnames.h index 81767f63..ba48c27b 100644 --- a/src/irisnet/corelib/netnames.h +++ b/src/irisnet/corelib/netnames.h @@ -501,36 +501,47 @@ class IRISNET_EXPORT NameResolver : public QObject { IRISNET_EXPORT QDebug operator<<(QDebug, XMPP::NameResolver::Error); +struct ServiceBoundRecord { + QString service; + NameRecord record; + + bool operator==(const ServiceBoundRecord &other) const + { + return service == other.service && record == other.record; + }; +}; + class IRISNET_EXPORT WeightedNameRecordList { friend QDebug operator<<(QDebug, const WeightedNameRecordList &); public: WeightedNameRecordList(); - WeightedNameRecordList(const QList &list); + WeightedNameRecordList(const QList &list); WeightedNameRecordList(const WeightedNameRecordList &other); WeightedNameRecordList &operator=(const WeightedNameRecordList &other); ~WeightedNameRecordList(); - bool isEmpty() const; //!< Returns true if the list contains no items; otherwise returns false. - NameRecord takeNext(); //!< Removes the next host to try from the list and returns it. + bool isEmpty() const; //!< Returns true if the list contains no items; otherwise returns false. + ServiceBoundRecord takeNext(); //!< Removes the next host to try from the list and returns it. void clear(); //!< Removes all items from the list. void append(const WeightedNameRecordList &); - void append(const QList &); - void append(const NameRecord &); + void append(const QList &); + void append(const ServiceBoundRecord &); void append(const QString &hostname, quint16 port); WeightedNameRecordList &operator<<(const WeightedNameRecordList &); - WeightedNameRecordList &operator<<(const QList &); - WeightedNameRecordList &operator<<(const NameRecord &); + WeightedNameRecordList &operator<<(const QList &); + WeightedNameRecordList &operator<<(const ServiceBoundRecord &); private: - typedef QMultiMap WeightedNameRecordPriorityGroup; + typedef QMultiMap WeightedNameRecordPriorityGroup; typedef std::map WNRL; WNRL priorityGroups; WNRL::iterator currentPriorityGroup; }; +QDebug operator<<(QDebug, const ServiceBoundRecord &); QDebug operator<<(QDebug, const XMPP::WeightedNameRecordList &); class IRISNET_EXPORT ServiceBrowser : public QObject { @@ -620,15 +631,15 @@ class IRISNET_EXPORT ServiceResolver : public QObject { * \param host Hostname to lookup * \param port Port to signal via resultReady (for convenience) */ - void start(const QString &host, quint16 port); + void start(const QString &host, quint16 port, const QString &service = {}); /*! * Start an indirect (SRV) lookup for the service - * \param service Service type, like "ssh" or "ftp" + * \param services List of services considered to be the same type, like "ssh" or "ftp" * \param transport IP transport, like "tcp" or "udp" * \param domain Domainname to lookup * \param port Specify a valid port number to make ServiceResolver fallback to domain:port */ - void start(const QString &service, const QString &transport, const QString &domain, + void start(const QStringList &services, const QString &transport, const QString &domain, int port = std::numeric_limits::max()); /*! Announce the next resolved host, \sa resultReady */ @@ -651,7 +662,7 @@ class IRISNET_EXPORT ServiceResolver : public QObject { * \param port Port the service resides on * \param hostname Hostname form DNS reply with address:port */ - void resultReady(const QHostAddress &address, quint16 port, const QString &hostname); + void resultReady(const QHostAddress &address, quint16 port, const QString &hostname, const QString &service); /*! The lookup failed */ void error(XMPP::ServiceResolver::Error); /*! SRV domain:port records received. No IP yet. */ @@ -659,9 +670,7 @@ class IRISNET_EXPORT ServiceResolver : public QObject { void srvFailed(); private slots: - void handle_srv_ready(const QList &); - void handle_srv_error(XMPP::NameResolver::Error); - void handle_host_ready(const QList &); + void handle_host_ready(const QString &service, const QList &); void handle_host_error(XMPP::NameResolver::Error); void handle_host_fallback_error(XMPP::NameResolver::Error); diff --git a/src/irisnet/noncore/cutestuff/bsocket.cpp b/src/irisnet/noncore/cutestuff/bsocket.cpp index 3f677077..0850c7bb 100644 --- a/src/irisnet/noncore/cutestuff/bsocket.cpp +++ b/src/irisnet/noncore/cutestuff/bsocket.cpp @@ -27,12 +27,23 @@ #include #include -#include +#include + +// #include // if it's still needed please comment why -// #define BS_DEBUG -#ifdef BS_DEBUG #define BSDEBUG (qDebug() << this << "#" << __FUNCTION__ << ":") -#endif +static std::optional enable_logs; +#define BSLOG(msg) \ + { \ + if (!enable_logs.has_value()) { \ + enable_logs = qgetenv("BS_DEBUG") == "1"; \ + } \ + if (*enable_logs) { \ + msg; \ + } \ + } \ + while (false) \ + ; #define READBUFSIZE 65536 @@ -83,11 +94,12 @@ class HappyEyeballsConnector : public QObject { QTcpSocketSignalRelay *relay; State state; QString hostname; // last resolved name + QString service; // one of services passed to service (SRV) resolver XMPP::ServiceResolver *resolver; }; /*! source data */ - QString service; + // QString service; QString transport; QString domain; quint16 port = 0; @@ -132,9 +144,7 @@ class HappyEyeballsConnector : public QObject { void connectToHost(const QHostAddress &address, quint16 port) { -#ifdef BS_DEBUG - BSDEBUG << "a:" << address << "p:" << port; -#endif + BSLOG(BSDEBUG << "a:" << address << "p:" << port); this->address = address; SockData &sd = addSocket(); sd.state = Connecting; @@ -144,9 +154,7 @@ class HappyEyeballsConnector : public QObject { /* Connect to a host via the specified protocol, or the default protocols if not specified */ void connectToHost(const QString &host, quint16 port, QAbstractSocket::NetworkLayerProtocol protocol) { -#ifdef BS_DEBUG - BSDEBUG << "h:" << host << "p:" << port << "pr:" << protocol; -#endif + BSLOG(BSDEBUG << "h:" << host << "p:" << port << "pr:" << protocol); this->domain = host; this->port = port; SockData &sd = addSocket(); @@ -175,23 +183,21 @@ class HappyEyeballsConnector : public QObject { } } - void connectToHost(const QString &service, const QString &transport, const QString &domain, quint16 port) + void connectToHost(const QStringList &services, const QString &transport, const QString &domain, quint16 port) { -#ifdef BS_DEBUG - BSDEBUG << "s:" << service << "t:" << transport << "d:" << domain; -#endif - this->service = service; + BSLOG(BSDEBUG << "s:" << services << "t:" << transport << "d:" << domain); + // this->service = service; this->transport = transport; this->domain = domain; this->port = port; SockData &sd = addSocket(); sd.resolver = new XMPP::ServiceResolver(this); sd.resolver->setProtocol(XMPP::ServiceResolver::HappyEyeballs); - connect(sd.resolver, SIGNAL(srvReady()), SLOT(splitSrvResolvers())); + connect(sd.resolver, &XMPP::ServiceResolver::srvReady, this, &HappyEyeballsConnector::splitSrvResolvers); // we don't care about special handling of fail. we have fallback host there anyway - connect(sd.resolver, SIGNAL(srvFailed()), SLOT(splitSrvResolvers())); + connect(sd.resolver, &XMPP::ServiceResolver::srvFailed, this, &HappyEyeballsConnector::splitSrvResolvers); sd.state = Resolve; - sd.resolver->start(service, transport, domain, port); + sd.resolver->start(services, transport, domain, port); } SockData takeCurrent(QObject *parent) @@ -258,9 +264,7 @@ private slots: */ void qs_connected() { -#ifdef BS_DEBUG - BSDEBUG; -#endif + BSLOG(BSDEBUG); QPointer valid(this); setCurrentByRelay(static_cast(sender())); for (int i = 0; i < sockets.count(); i++) { @@ -281,9 +285,8 @@ private slots: setCurrentByRelay(static_cast(sender())); // TODO remember error code lastError = sockets[lastIndex].sock->errorString(); -#ifdef BS_DEBUG - BSDEBUG << "error:" << lastError; -#endif + BSLOG(BSDEBUG << "error:" << lastError); + if (sockets[lastIndex].resolver) { sockets[lastIndex].sock->abort(); sockets[lastIndex].state = Resolve; @@ -296,13 +299,15 @@ private slots: void splitSrvResolvers() { -#ifdef BS_DEBUG - BSDEBUG << "splitting resolvers"; -#endif + BSLOG(BSDEBUG << "splitting resolvers"); setCurrentByResolver(static_cast(sender())); - SockData &sdv4 = sockets[lastIndex]; - SockData &sdv6 = addSocket(); - XMPP::ServiceResolver::ProtoSplit ps = sdv4.resolver->happySplit(); + Q_ASSERT(lastIndex >= 0); + + auto tmp = lastIndex; + SockData &sdv6 = addSocket(); + SockData &sdv4 = sockets[tmp]; + + XMPP::ServiceResolver::ProtoSplit ps = sdv4.resolver->happySplit(); initResolver(ps.ipv4); initResolver(ps.ipv6); @@ -324,25 +329,20 @@ private slots: } /* host resolved, now try to connect to it */ - void handleDnsReady(const QHostAddress &address, quint16 port, const QString &hostname) + void handleDnsReady(const QHostAddress &address, quint16 port, const QString &hostname, const QString &service) { -#ifdef BS_DEBUG - BSDEBUG << "a:" << address << "p:" << port; -#endif + BSLOG(BSDEBUG << "a:" << address << "p:" << port); setCurrentByResolver(static_cast(sender())); sockets[lastIndex].state = Connecting; sockets[lastIndex].hostname = hostname; + sockets[lastIndex].service = service; sockets[lastIndex].sock->connectToHost(address, port); } /* resolver failed the dns lookup */ void handleDnsError(XMPP::ServiceResolver::Error e) { -#ifdef BS_DEBUG - BSDEBUG << "e:" << e; -#else - Q_UNUSED(e) -#endif + BSLOG(BSDEBUG << "e:" << e); if (!fallbackTimer.isActive()) { emit error(QAbstractSocket::HostNotFoundError); } @@ -350,9 +350,7 @@ private slots: void startFallback() { -#ifdef BS_DEBUG - BSDEBUG; -#endif + BSLOG(BSDEBUG); for (int i = 0; i < sockets.count(); i++) { SockData &sd = sockets[i]; if (sd.state == Created) { @@ -388,6 +386,7 @@ class BSocket::Private { QTcpSocketSignalRelay *qsock_relay; int state; + QString service; //!< One of passed to BSocket::connectToHost(QList) QString domain; //!< Domain we are currently connected to QString host; //!< Hostname we are currently connected to QHostAddress address; //!< IP address we are currently connected to @@ -410,9 +409,7 @@ BSocket::~BSocket() void BSocket::resetConnection(bool clear) { -#ifdef BS_DEBUG - BSDEBUG << clear; -#endif + BSLOG(BSDEBUG << clear); if (d->connector) { d->connector->deleteLater(); disconnect(d->connector); @@ -462,6 +459,7 @@ void BSocket::ensureConnector() /* Connect to an already resolved host */ void BSocket::connectToHost(const QHostAddress &address, quint16 port) { + BSLOG(BSDEBUG << address << port); resetConnection(true); d->address = address; d->port = port; @@ -474,6 +472,7 @@ void BSocket::connectToHost(const QHostAddress &address, quint16 port) /* Connect to a host via the specified protocol, or the default protocols if not specified */ void BSocket::connectToHost(const QString &host, quint16 port, QAbstractSocket::NetworkLayerProtocol protocol) { + BSLOG(BSDEBUG << host << port << protocol); resetConnection(true); d->host = host; d->port = port; @@ -483,15 +482,16 @@ void BSocket::connectToHost(const QString &host, quint16 port, QAbstractSocket:: d->connector->connectToHost(host, port, protocol); } -/* Connect to the hosts for the specified service */ -void BSocket::connectToHost(const QString &service, const QString &transport, const QString &domain, quint16 port) +/* Connect to the hosts for the specified services */ +void BSocket::connectToHost(const QStringList &services, const QString &transport, const QString &domain, quint16 port) { + BSLOG(BSDEBUG << services << transport << domain << port); resetConnection(true); d->domain = domain; d->state = Connecting; ensureConnector(); - d->connector->connectToHost(service, transport, domain, port); + d->connector->connectToHost(services, transport, domain, port); } QAbstractSocket *BSocket::abstractSocket() const { return d->qsock; } @@ -547,9 +547,7 @@ qint64 BSocket::writeData(const char *data, qint64 maxSize) { if (d->state != Connected) return 0; -#ifdef BS_DEBUG_EXTRA - BSDEBUG << "- [" << maxSize << "]: {" << QByteArray::fromRawData(data, maxSize) << "}"; -#endif + BSLOG(BSDEBUG << "- [" << maxSize << "]: {" << QByteArray::fromRawData(data, maxSize) << "}"); return d->qsock->write(data, maxSize); } @@ -569,9 +567,7 @@ qint64 BSocket::readData(char *data, qint64 maxSize) readSize = ByteStream::readData(data, maxSize); } -#ifdef BS_DEBUG_EXTRA - BSDEBUG << "- [" << readSize << "]: {" << QByteArray::fromRawData(data, readSize) << "}"; -#endif + BSLOG(BSDEBUG << "- [" << readSize << "]: {" << QByteArray::fromRawData(data, readSize) << "}"); return readSize; } @@ -622,12 +618,15 @@ quint16 BSocket::peerPort() const return 0; } +QString BSocket::service() const { return d->service; } + void BSocket::qs_connected() { HappyEyeballsConnector::SockData sd = d->connector->takeCurrent(this); d->qsock = sd.sock; d->qsock_relay = sd.relay; d->host = sd.hostname; + d->service = sd.service; d->connector->deleteLater(); qs_connected_step2(true); } @@ -641,9 +640,7 @@ void BSocket::qs_connected_step2(bool signalConnected) setOpenMode(QIODevice::ReadWrite); d->state = Connected; -#ifdef BS_DEBUG - BSDEBUG << "Connected"; -#endif + BSLOG(BSDEBUG << "Connected"); QPointer valid(this); if (signalConnected) { emit connected(); @@ -657,9 +654,7 @@ void BSocket::qs_connected_step2(bool signalConnected) void BSocket::qs_closed() { if (d->state == Closing) { -#ifdef BS_DEBUG - BSDEBUG << "Delayed Close Finished"; -#endif + BSLOG(BSDEBUG << "Delayed Close Finished"); resetConnection(); emit delayedCloseFinished(); } @@ -669,26 +664,20 @@ void BSocket::qs_readyRead() { emit readyRead(); } void BSocket::qs_bytesWritten(qint64 x64) { -#ifdef BS_DEBUG_EXTRA - BSDEBUG << "BytesWritten [" << x64 << "]"; -#endif + BSLOG(BSDEBUG << "BytesWritten [" << x64 << "]"); emit bytesWritten(x64); } void BSocket::qs_error(QAbstractSocket::SocketError x) { if (x == QTcpSocket::RemoteHostClosedError) { -#ifdef BS_DEBUG - BSDEBUG << "Connection Closed"; -#endif + BSLOG(BSDEBUG << "Connection Closed"); resetConnection(); emit connectionClosed(); return; } -#ifdef BS_DEBUG - BSDEBUG << "Error"; -#endif + BSLOG(BSDEBUG << "Error"); resetConnection(); if (x == QTcpSocket::ConnectionRefusedError) emit error(ErrConnectionRefused); diff --git a/src/irisnet/noncore/cutestuff/bsocket.h b/src/irisnet/noncore/cutestuff/bsocket.h index 17498585..99f68dd2 100644 --- a/src/irisnet/noncore/cutestuff/bsocket.h +++ b/src/irisnet/noncore/cutestuff/bsocket.h @@ -49,8 +49,9 @@ class BSocket : public ByteStream { void connectToHost(const QString &host, quint16 port, QAbstractSocket::NetworkLayerProtocol protocol = QAbstractSocket::UnknownNetworkLayerProtocol); /*! Connect to the hosts for the specified service */ - void connectToHost(const QString &service, const QString &transport, const QString &domain, - quint16 port = std::numeric_limits::max()); + void connectToHost(const QStringList &services, const QString &transport, const QString &domain, + quint16 port = std::numeric_limits::max()); + virtual QAbstractSocket *abstractSocket() const; qintptr socket() const; void setSocket(QTcpSocket *); @@ -72,6 +73,8 @@ class BSocket : public ByteStream { QHostAddress peerAddress() const; quint16 peerPort() const; + QString service() const; + protected: qint64 writeData(const char *data, qint64 maxSize); qint64 readData(char *data, qint64 maxSize); diff --git a/src/irisnet/noncore/icelocaltransport.cpp b/src/irisnet/noncore/icelocaltransport.cpp index b0eb4870..9c69079d 100644 --- a/src/irisnet/noncore/icelocaltransport.cpp +++ b/src/irisnet/noncore/icelocaltransport.cpp @@ -434,7 +434,7 @@ private slots: QByteArray buf = sock->readDatagram(from); if (buf.isEmpty()) // it's weird we ever came here, but should relax static analyzer break; - qDebug("got packet from %s", qPrintable(from)); + // qDebug("got packet from %s", qPrintable(from)); if (from == stunBindAddr || from == stunRelayAddr) { bool haveData = processIncomingStun(buf, from, &dg); diff --git a/src/irisnet/noncore/sctp/SctpAssociation.cpp b/src/irisnet/noncore/sctp/SctpAssociation.cpp index eade02d3..ba9aebf3 100644 --- a/src/irisnet/noncore/sctp/SctpAssociation.cpp +++ b/src/irisnet/noncore/sctp/SctpAssociation.cpp @@ -789,8 +789,8 @@ void SctpAssociation::OnUsrSctpReceiveSctpNotification(union sctp_notification * case SCTP_STREAM_RESET_EVENT: { bool incoming { false }; bool outgoing { false }; - uint16_t numStreams = (notification->sn_strreset_event.strreset_length - sizeof(struct sctp_stream_reset_event)) - / sizeof(uint16_t); + uint16_t numStreams = uint16_t((notification->sn_strreset_event.strreset_length - sizeof(struct sctp_stream_reset_event)) + / sizeof(uint16_t)); if (notification->sn_strreset_event.strreset_flags & SCTP_STREAM_RESET_INCOMING_SSN) incoming = true; diff --git a/src/irisnet/noncore/sctp/SctpAssociation.hpp b/src/irisnet/noncore/sctp/SctpAssociation.hpp index 1e371f6a..70fd5405 100644 --- a/src/irisnet/noncore/sctp/SctpAssociation.hpp +++ b/src/irisnet/noncore/sctp/SctpAssociation.hpp @@ -36,7 +36,7 @@ class SctpAssociation { virtual void OnSctpAssociationMessageReceived(RTC::SctpAssociation *sctpAssociation, uint16_t streamId, uint32_t ppid, const uint8_t *msg, size_t len) = 0; - virtual void OnSctpAssociationBufferedAmount(RTC::SctpAssociation *sctpAssociation, uint32_t len) = 0; + virtual void OnSctpAssociationBufferedAmount(RTC::SctpAssociation *sctpAssociation, size_t len) = 0; virtual void OnSctpStreamClosed(RTC::SctpAssociation *sctpAssociation, uint16_t streamId) = 0; }; diff --git a/src/xmpp/CMakeLists.txt b/src/xmpp/CMakeLists.txt index 5c4cffec..963ef04f 100644 --- a/src/xmpp/CMakeLists.txt +++ b/src/xmpp/CMakeLists.txt @@ -56,6 +56,7 @@ set(XMPP_IM_HEADERS xmpp-im/xmpp_subsets.h xmpp-im/xmpp_url.h xmpp-im/xmpp_vcard.h + xmpp-im/xmpp_vcard4.h xmpp-im/xmpp_xdata.h xmpp-im/xmpp_xmlcommon.h xmpp-im/xmpp_encryption.h @@ -97,7 +98,6 @@ set(XMPP_HEADERS_PRIVATE sasl/scramsha1signature.h zlib/zlibcompressor.h zlib/zlibdecompressor.h - blake2/blake2qt.h base/timezone.h ) @@ -135,6 +135,7 @@ target_sources(iris PRIVATE xmpp-im/xmpp_task.cpp xmpp-im/xmpp_tasks.cpp xmpp-im/xmpp_vcard.cpp + xmpp-im/xmpp_vcard4.cpp xmpp-im/xmpp_xdata.cpp xmpp-im/xmpp_xmlcommon.cpp xmpp-im/xmpp_encryption.cpp @@ -158,8 +159,6 @@ target_sources(iris PRIVATE zlib/zlibcompressor.cpp zlib/zlibdecompressor.cpp - blake2/blake2qt.cpp - jid/jid.cpp sasl/digestmd5proplist.cpp @@ -187,17 +186,20 @@ if(IRIS_ENABLE_JINGLE_SCTP) ) endif() -if(B2_FOUND) - message(STATUS "Building with system blake2 library") - target_link_libraries(iris PRIVATE ${B2_LIBRARY}) -else() - if(NOT IRIS_BUNDLED_QCA) - message(STATUS "No system blake2 and bundled QCA is disabled. Expect slow hashing.") +if(QT_DEFAULT_MAJOR_VERSION LESS 6) + target_sources(iris PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/blake2/blake2qt.cpp ${CMAKE_CURRENT_SOURCE_DIR}/blake2/blake2qt.h) + if(B2_FOUND) + message(STATUS "Building with system blake2 library") + target_link_libraries(iris PRIVATE ${B2_LIBRARY}) + else() + if(NOT IRIS_BUNDLED_QCA) + message(STATUS "No system blake2 and bundled QCA is disabled. Expect slow hashing.") + endif() + target_sources(iris PRIVATE + blake2/blake2b-ref.c + blake2/blake2s-ref.c + ) endif() - target_sources(iris PRIVATE - blake2/blake2b-ref.c - blake2/blake2s-ref.c - ) endif() target_link_libraries(iris @@ -212,7 +214,10 @@ target_link_libraries(iris if(IRIS_BUNDLED_QCA) add_dependencies(iris QcaProject) - target_include_directories(iris PUBLIC ${Qca_INCLUDE_DIR}) + target_include_directories(iris PUBLIC + "$" + "$" + ) target_link_libraries(iris PUBLIC OpenSSL::SSL) endif() diff --git a/src/xmpp/jid/jid.cpp b/src/xmpp/jid/jid.cpp index 5295e58b..74bc72a1 100644 --- a/src/xmpp/jid/jid.cpp +++ b/src/xmpp/jid/jid.cpp @@ -307,6 +307,9 @@ void Jid::setNode(const QString &s) void Jid::setResource(const QString &s) { + if (r == s) { + return; + } if (!valid) return; QString norm; diff --git a/src/xmpp/sasl/scramsha1response.cpp b/src/xmpp/sasl/scramsha1response.cpp index 22f92a31..2c73bdce 100644 --- a/src/xmpp/sasl/scramsha1response.cpp +++ b/src/xmpp/sasl/scramsha1response.cpp @@ -37,7 +37,6 @@ QCA::SecureArray HMAC_SHA_1(const QCA::SecureArray &key, const QCA::SecureArray SCRAMSHA1Response::SCRAMSHA1Response(const QByteArray &server_first_message, const QByteArray &password_in, const QByteArray &client_first_message, const QString &salted_password_base64) { - Q_UNUSED(rand); QString pass_in = QString::fromUtf8(password_in); QString pass_out; diff --git a/src/xmpp/xmpp-core/connector.cpp b/src/xmpp/xmpp-core/connector.cpp index f73e8bd5..dc0712fc 100644 --- a/src/xmpp/xmpp-core/connector.cpp +++ b/src/xmpp/xmpp-core/connector.cpp @@ -51,6 +51,7 @@ using namespace XMPP; static const int XMPP_DEFAULT_PORT = 5222; static const int XMPP_LEGACY_PORT = 5223; static const char *XMPP_CLIENT_SRV = "xmpp-client"; +static const char *XMPP_CLIENT_TLS_SRV = "xmpps-client"; static const char *XMPP_CLIENT_TRANSPORT = "tcp"; //---------------------------------------------------------------------------- @@ -148,39 +149,33 @@ AdvancedConnector::Proxy::operator QNetworkProxy() // AdvancedConnector //---------------------------------------------------------------------------- typedef enum { Idle, Connecting, Connected } Mode; -typedef enum { Force, Probe, Never } LegacySSL; +typedef enum : char { Force, Never } DirectTLS; class AdvancedConnector::Private { public: - ByteStream *bs; //!< Socket to use + ByteStream *bs = nullptr; //!< Socket to use /* configuration values / "options" */ - QString opt_host; //!< explicit host from config - quint16 opt_port; //!< explicit port from config - LegacySSL opt_ssl; //!< Whether to use legacy SSL support - Proxy proxy; //!< Proxy configuration + QString opt_host; //!< explicit host from config + quint16 opt_port; //!< explicit port from config + bool opt_directtls = false; //!< Whether to use direct TLS support + bool opt_srvtls = true; //!< Whether to lookup tls port from SRV + Proxy proxy; //!< Proxy configuration /* State tracking values */ - Mode mode; //!< Idle, Connecting, Connected - QString host; //!< Host we currently try to connect to, set from connectToServer() - int port; //!< Port we currently try to connect to, set from connectToServer() and bs_error() - int errorCode; //!< Current error, if any + Mode mode; //!< Idle, Connecting, Connected + QString host; //!< Host we currently try to connect to, set from connectToServer() + int port; //!< Port we currently try to connect to, set from connectToServer() and bs_error() + int errorCode = 0; //!< Current error, if any }; -AdvancedConnector::AdvancedConnector(QObject *parent) : Connector(parent) +AdvancedConnector::AdvancedConnector(QObject *parent) : Connector(parent), d(new Private) { - d = new Private; - d->bs = nullptr; - d->opt_ssl = Never; cleanup(); d->errorCode = 0; } -AdvancedConnector::~AdvancedConnector() -{ - cleanup(); - delete d; -} +AdvancedConnector::~AdvancedConnector() { cleanup(); } void AdvancedConnector::cleanup() { @@ -219,7 +214,7 @@ void AdvancedConnector::setOptHostPort(const QString &_host, quint16 _port) d->opt_port = _port; } -void AdvancedConnector::setOptProbe(bool b) +void AdvancedConnector::setOptSSL(bool b) { #ifdef XMPP_DEBUG XDEBUG << "b:" << b; @@ -227,18 +222,17 @@ void AdvancedConnector::setOptProbe(bool b) if (d->mode != Idle) return; - d->opt_ssl = (b ? Probe : Never); + d->opt_directtls = b; } -void AdvancedConnector::setOptSSL(bool b) +void AdvancedConnector::setOptTlsSrv(bool value) { #ifdef XMPP_DEBUG XDEBUG << "b:" << b; #endif - if (d->mode != Idle) return; - d->opt_ssl = (b ? Force : Never); + d->opt_srvtls = value; } void AdvancedConnector::connectToServer(const QString &server) @@ -263,13 +257,6 @@ void AdvancedConnector::connectToServer(const QString &server) } d->port = XMPP_DEFAULT_PORT; - if (d->opt_ssl == Probe && (d->proxy.type() != Proxy::None || !d->opt_host.isEmpty())) { -#ifdef XMPP_DEBUG - XDEBUG << "Don't probe ssl port because of incompatible params"; -#endif - d->opt_ssl = Never; // probe is possible only with direct connect - } - if (d->proxy.type() == Proxy::HttpPoll) { HttpPoll *s = new HttpPoll; d->bs = s; @@ -326,7 +313,12 @@ void AdvancedConnector::connectToServer(const QString &server) XDEBUG << "Adding socket:" << s; #endif - connect(s, SIGNAL(connected()), SLOT(bs_connected())); + connect(s, &BSocket::connected, this, [this, s]() { + if (!useSSL()) { + setUseSSL(s->service() == QLatin1String(XMPP_CLIENT_TLS_SRV)); + } + bs_connected(); + }); connect(s, SIGNAL(error(int)), SLOT(bs_error(int))); if (!d->opt_host.isEmpty()) { /* if custom host:port */ @@ -334,11 +326,17 @@ void AdvancedConnector::connectToServer(const QString &server) d->port = d->opt_port; s->connectToHost(d->host, quint16(d->port)); return; - } else if (d->opt_ssl != Never) { /* if ssl forced or should be probed */ - d->port = XMPP_LEGACY_PORT; } - s->connectToHost(XMPP_CLIENT_SRV, XMPP_CLIENT_TRANSPORT, d->host, quint16(d->port)); + QStringList services; + if (!d->opt_directtls && d->opt_srvtls) { + services << XMPP_CLIENT_TLS_SRV; + } + services << XMPP_CLIENT_SRV; + if (d->opt_directtls) { + d->port = XMPP_LEGACY_PORT; + } + s->connectToHost(services, XMPP_CLIENT_TRANSPORT, d->host, quint16(d->port)); } } @@ -373,11 +371,9 @@ void AdvancedConnector::bs_connected() setPeerAddress(h, p); } - // We won't use ssl with HttpPoll since it has ow tls handler enabled for https. + // We won't use ssl with HttpPoll since it has own tls handler enabled for https. // The only variant for ssl is legacy port in probing or forced mde. - if (d->proxy.type() != Proxy::HttpPoll - && (d->opt_ssl == Force || (d->opt_ssl == Probe && peerPort() == XMPP_LEGACY_PORT))) { - // in case of Probe it's ok to check actual peer "port" since we are sure Proxy=None + if (d->proxy.type() != Proxy::HttpPoll && (d->opt_directtls || peerPort() == XMPP_LEGACY_PORT)) { setUseSSL(true); } @@ -466,28 +462,12 @@ void AdvancedConnector::bs_error(int x) return; } - /* - if we shall probe the ssl legacy port, and we just did that (port=legacy), - then try to connect to the normal port instead - */ - if (d->opt_ssl == Probe && d->port == XMPP_LEGACY_PORT) { #ifdef XMPP_DEBUG - qDebug("bse1.2"); + qDebug("bse1.3"); #endif - BSocket *s = static_cast(d->bs); - d->port = XMPP_DEFAULT_PORT; - // at this moment we already tried everything from srv. so just try the host itself - s->connectToHost(d->host, quint16(d->port)); - } - /* otherwise we have no fallbacks and must have failed to connect */ - else { -#ifdef XMPP_DEBUG - qDebug("bse1.3"); -#endif - cleanup(); - d->errorCode = ErrConnectionRefused; - emit error(); - } + cleanup(); + d->errorCode = ErrConnectionRefused; + emit error(); } void AdvancedConnector::http_syncStarted() { emit httpSyncStarted(); } diff --git a/src/xmpp/xmpp-core/protocol.cpp b/src/xmpp/xmpp-core/protocol.cpp index 9cb3aca3..157e8d42 100644 --- a/src/xmpp/xmpp-core/protocol.cpp +++ b/src/xmpp/xmpp-core/protocol.cpp @@ -30,10 +30,12 @@ #include #include #include +#include #include using namespace XMPP; +#ifdef XMPP_TEST // printArray // // This function prints out an array of bytes as latin characters, converting @@ -52,6 +54,7 @@ static QString printArray(const QByteArray &a) } return s; } +#endif // firstChildElement // @@ -92,48 +95,46 @@ StreamFeatures::StreamFeatures() //---------------------------------------------------------------------------- // BasicProtocol //---------------------------------------------------------------------------- -BasicProtocol::SASLCondEntry BasicProtocol::saslCondTable[] = { - { "aborted", Aborted }, - { "account-disabled", AccountDisabled }, - { "credentials-expired", CredentialsExpired }, - { "encryption-required", EncryptionRequired }, - { "incorrect-encoding", IncorrectEncoding }, - { "invalid-authzid", InvalidAuthzid }, - { "invalid-mechanism", InvalidMech }, - { "malformed-request", MalformedRequest }, - { "mechanism-too-weak", MechTooWeak }, - { "not-authorized", NotAuthorized }, - { "temporary-auth-failure", TemporaryAuthFailure }, - { nullptr, 0 }, -}; - -BasicProtocol::StreamCondEntry BasicProtocol::streamCondTable[] = { - { "bad-format", BadFormat }, - { "bad-namespace-prefix", BadNamespacePrefix }, - { "conflict", Conflict }, - { "connection-timeout", ConnectionTimeout }, - { "host-gone", HostGone }, - { "host-unknown", HostUnknown }, - { "improper-addressing", ImproperAddressing }, - { "internal-server-error", InternalServerError }, - { "invalid-from", InvalidFrom }, - { "invalid-namespace", InvalidNamespace }, - { "invalid-xml", InvalidXml }, - { "not-authorized", StreamNotAuthorized }, - { "not-well-formed", NotWellFormed }, - { "policy-violation", PolicyViolation }, - { "remote-connection-failed", RemoteConnectionFailed }, - { "reset", StreamReset }, - { "resource-constraint", ResourceConstraint }, - { "restricted-xml", RestrictedXml }, - { "see-other-host", SeeOtherHost }, - { "system-shutdown", SystemShutdown }, - { "undefined-condition", UndefinedCondition }, - { "unsupported-encoding", UnsupportedEncoding }, - { "unsupported-stanza-type", UnsupportedStanzaType }, - { "unsupported-version", UnsupportedVersion }, - { nullptr, 0 }, -}; +std::array BasicProtocol::saslCondTable { { + { QStringLiteral("aborted"), SASLCond::Aborted }, + { QStringLiteral("account-disabled"), SASLCond::AccountDisabled }, + { QStringLiteral("credentials-expired"), SASLCond::CredentialsExpired }, + { QStringLiteral("encryption-required"), SASLCond::EncryptionRequired }, + { QStringLiteral("incorrect-encoding"), SASLCond::IncorrectEncoding }, + { QStringLiteral("invalid-authzid"), SASLCond::InvalidAuthzid }, + { QStringLiteral("invalid-mechanism"), SASLCond::InvalidMechanism }, + { QStringLiteral("malformed-request"), SASLCond::MalformedRequest }, + { QStringLiteral("mechanism-too-weak"), SASLCond::MechanismTooWeak }, + { QStringLiteral("not-authorized"), SASLCond::NotAuthorized }, + { QStringLiteral("temporary-auth-failure"), SASLCond::TemporaryAuthFailure }, +} }; + +std::array BasicProtocol::streamCondTable { { + { QStringLiteral("bad-format"), StreamCond::BadFormat }, + { QStringLiteral("bad-namespace-prefix"), StreamCond::BadNamespacePrefix }, + { QStringLiteral("conflict"), StreamCond::Conflict }, + { QStringLiteral("connection-timeout"), StreamCond::ConnectionTimeout }, + { QStringLiteral("host-gone"), StreamCond::HostGone }, + { QStringLiteral("host-unknown"), StreamCond::HostUnknown }, + { QStringLiteral("improper-addressing"), StreamCond::ImproperAddressing }, + { QStringLiteral("internal-server-error"), StreamCond::InternalServerError }, + { QStringLiteral("invalid-from"), StreamCond::InvalidFrom }, + { QStringLiteral("invalid-namespace"), StreamCond::InvalidNamespace }, + { QStringLiteral("invalid-xml"), StreamCond::InvalidXml }, + { QStringLiteral("not-authorized"), StreamCond::NotAuthorized }, + { QStringLiteral("not-well-formed"), StreamCond::NotWellFormed }, + { QStringLiteral("policy-violation"), StreamCond::PolicyViolation }, + { QStringLiteral("remote-connection-failed"), StreamCond::RemoteConnectionFailed }, + { QStringLiteral("reset"), StreamCond::Reset }, + { QStringLiteral("resource-constraint"), StreamCond::ResourceConstraint }, + { QStringLiteral("restricted-xml"), StreamCond::RestrictedXml }, + { QStringLiteral("see-other-host"), StreamCond::SeeOtherHost }, + { QStringLiteral("system-shutdown"), StreamCond::SystemShutdown }, + { QStringLiteral("undefined-condition"), StreamCond::UndefinedCondition }, + { QStringLiteral("unsupported-encoding"), StreamCond::UnsupportedEncoding }, + { QStringLiteral("unsupported-stanza-type"), StreamCond::UnsupportedStanzaType }, + { QStringLiteral("unsupported-version"), StreamCond::UnsupportedVersion }, +} }; BasicProtocol::BasicProtocol() : XmlProtocol() { init(); } @@ -141,7 +142,7 @@ BasicProtocol::~BasicProtocol() { } void BasicProtocol::init() { - errCond = -1; + errCond = {}; sasl_authed = false; doShutdown = false; delayedError = false; @@ -208,7 +209,7 @@ QDomElement BasicProtocol::recvStanza() void BasicProtocol::shutdown() { doShutdown = true; } -void BasicProtocol::shutdownWithError(int cond, const QString &str) +void BasicProtocol::shutdownWithError(StreamCond cond, const QString &str) { otherHost = str; delayErrorAndClose(cond); @@ -234,38 +235,38 @@ void BasicProtocol::setSASLNext(const QByteArray &step) { sasl_step = step; } void BasicProtocol::setSASLAuthed() { sasl_authed = true; } -int BasicProtocol::stringToSASLCond(const QString &s) +std::optional BasicProtocol::stringToSASLCond(const QString &s) { - for (int n = 0; saslCondTable[n].str; ++n) { - if (s == saslCondTable[n].str) - return saslCondTable[n].cond; + for (auto const &entry : saslCondTable) { + if (s == entry.str) + return entry.cond; } - return -1; + return {}; } -int BasicProtocol::stringToStreamCond(const QString &s) +std::optional BasicProtocol::stringToStreamCond(const QString &s) { - for (int n = 0; streamCondTable[n].str; ++n) { - if (s == streamCondTable[n].str) - return streamCondTable[n].cond; + for (auto const &entry : streamCondTable) { + if (s == entry.str) + return entry.cond; } - return -1; + return {}; } -QString BasicProtocol::saslCondToString(int x) +QString BasicProtocol::saslCondToString(SASLCond x) { - for (int n = 0; saslCondTable[n].str; ++n) { - if (x == saslCondTable[n].cond) - return saslCondTable[n].str; + for (auto const &entry : saslCondTable) { + if (x == entry.cond) + return entry.str; } return QString(); } -QString BasicProtocol::streamCondToString(int x) +QString BasicProtocol::streamCondToString(StreamCond x) { - for (int n = 0; streamCondTable[n].str; ++n) { - if (x == streamCondTable[n].cond) - return streamCondTable[n].str; + for (auto const &entry : streamCondTable) { + if (x == entry.cond) + return entry.str; } return QString(); } @@ -279,46 +280,49 @@ void BasicProtocol::extractStreamError(const QDomElement &e) QDomElement t = firstChildElement(e); if (t.isNull() || t.namespaceURI() != NS_STREAMS) { // probably old-style error - errCond = -1; + errCond = {}; errText = e.text(); } else errCond = stringToStreamCond(t.tagName()); - if (errCond != -1) { - if (errCond == SeeOtherHost) - otherHost = t.text(); - - auto nodes = e.elementsByTagNameNS(NS_STREAMS, "text"); - if (nodes.count()) { - for (int i = 0; i < nodes.count(); i++) { - auto e = nodes.item(i).toElement(); - QString lang = e.attributeNS(NS_STREAMS, "lang", ""); - langText.insert(lang, e.text()); - } - } else - text = t.text(); - - // find first non-standard namespaced element - QDomNodeList nl = e.childNodes(); - for (int n = 0; n < nl.count(); ++n) { - QDomNode i = nl.item(n); - if (i.isElement() && i.namespaceURI() != NS_STREAMS) { - appSpec = i.toElement(); - break; - } + if (!errCond.has_value()) { + qWarning("unknown stream error=%s", qUtf8Printable(t.tagName())); + // rfc6120 says we should treat unknows conditions as + errCond = StreamCond::UndefinedCondition; + } + if (std::get(*errCond) == StreamCond::SeeOtherHost) + otherHost = t.text(); + + auto nodes = e.elementsByTagNameNS(NS_STREAMS, "text"); + if (nodes.count()) { + for (int i = 0; i < nodes.count(); i++) { + auto e = nodes.item(i).toElement(); + QString lang = e.attributeNS(NS_STREAMS, "lang", ""); + langText.insert(lang, e.text()); + } + } else + text = t.text(); + + // find first non-standard namespaced element + QDomNodeList nl = e.childNodes(); + for (int n = 0; n < nl.count(); ++n) { + QDomNode i = nl.item(n); + if (i.isElement() && i.namespaceURI() != NS_STREAMS) { + appSpec = i.toElement(); + break; } - - errText = text; - errLangText = langText; - errAppSpec = appSpec; } + + errText = text; + errLangText = langText; + errAppSpec = appSpec; } void BasicProtocol::send(const QDomElement &e, bool clip) { writeElement(e, TypeElement, false, clip, false); } void BasicProtocol::sendUrgent(const QDomElement &e, bool clip) { writeElement(e, TypeElement, false, clip, true); } -void BasicProtocol::sendStreamError(int cond, const QString &text, const QDomElement &appSpec) +void BasicProtocol::sendStreamError(StreamCond cond, const QString &text, const QDomElement &appSpec) { QDomElement se = doc.createElementNS(NS_ETHERX, "stream:error"); QDomElement err = doc.createElementNS(NS_STREAMS, streamCondToString(cond)); @@ -344,7 +348,7 @@ void BasicProtocol::sendStreamError(const QString &text) writeElement(se, 100, false); } -bool BasicProtocol::errorAndClose(int cond, const QString &text, const QDomElement &appSpec) +bool BasicProtocol::errorAndClose(StreamCond cond, const QString &text, const QDomElement &appSpec) { closeError = true; errCond = cond; @@ -361,7 +365,7 @@ bool BasicProtocol::error(int code) return true; } -void BasicProtocol::delayErrorAndClose(int cond, const QString &text, const QDomElement &appSpec) +void BasicProtocol::delayErrorAndClose(StreamCond cond, const QString &text, const QDomElement &appSpec) { errorCode = ErrStream; errCond = cond; @@ -413,7 +417,7 @@ void BasicProtocol::handleDocOpen(const Parser::Event &pe) { if (isIncoming()) { if (xmlEncoding() != "UTF-8") { - delayErrorAndClose(UnsupportedEncoding); + delayErrorAndClose(StreamCond::UnsupportedEncoding); return; } } @@ -453,7 +457,7 @@ void BasicProtocol::handleDocOpen(const Parser::Event &pe) handleStreamOpen(pe); } else { if (isIncoming()) - delayErrorAndClose(BadFormat); + delayErrorAndClose(StreamCond::BadFormat); else delayError(ErrProtocol); } @@ -462,7 +466,7 @@ void BasicProtocol::handleDocOpen(const Parser::Event &pe) bool BasicProtocol::handleError() { if (isIncoming()) - return errorAndClose(NotWellFormed); + return errorAndClose(StreamCond::NotWellFormed); else return error(ErrParse); } @@ -483,7 +487,8 @@ bool BasicProtocol::doStep(const QDomElement &e) // handle pending error if (delayedError) { if (isIncoming()) - return errorAndClose(errCond, errText, errAppSpec); + // see delayErrorAndClose. it's the only place we set errCond + return errorAndClose(std::get(*errCond), errText, errAppSpec); else return error(errorCode); } @@ -787,6 +792,8 @@ void CoreProtocol::stringSend(const QString &s) { #ifdef XMPP_TEST TD::outgoingTag(s); +#else + Q_UNUSED(s) #endif } @@ -794,6 +801,8 @@ void CoreProtocol::stringRecv(const QString &s) { #ifdef XMPP_TEST TD::incomingTag(s); +#else + Q_UNUSED(s) #endif } @@ -828,13 +837,13 @@ void CoreProtocol::handleStreamOpen(const Parser::Event &pe) // verify namespace if ((!server && ns != NS_CLIENT) || (server && ns != NS_SERVER) || (dialback && db != NS_DIALBACK)) { - delayErrorAndClose(InvalidNamespace); + delayErrorAndClose(StreamCond::InvalidNamespace); return; } // verify version if (version.major < 1 && !dialback) { - delayErrorAndClose(UnsupportedVersion); + delayErrorAndClose(StreamCond::UnsupportedVersion); return; } } else { @@ -848,6 +857,8 @@ void CoreProtocol::elementSend(const QDomElement &e) { #ifdef XMPP_TEST TD::outgoingXml(e); +#else + Q_UNUSED(e) #endif } @@ -855,6 +866,8 @@ void CoreProtocol::elementRecv(const QDomElement &e) { #ifdef XMPP_TEST TD::incomingXml(e); +#else + Q_UNUSED(e) #endif } @@ -1464,7 +1477,7 @@ bool CoreProtocol::normalStep(const QDomElement &e) } else if (e.tagName() == "failure") { QDomElement t = firstChildElement(e); if (t.isNull() || t.namespaceURI() != NS_SASL) - errCond = -1; + errCond = {}; else errCond = stringToSASLCond(t.tagName()); @@ -1508,7 +1521,7 @@ bool CoreProtocol::normalStep(const QDomElement &e) jid_ = j; return loginComplete(); } else { - errCond = -1; + errCond = {}; QDomElement err = e.elementsByTagNameNS(NS_CLIENT, "error").item(0).toElement(); if (!err.isNull()) { @@ -1525,9 +1538,9 @@ bool CoreProtocol::normalStep(const QDomElement &e) if (!t.isNull() && t.namespaceURI() == NS_STANZAS) { QString cond = t.tagName(); if (cond == "not-allowed") - errCond = BindNotAllowed; + errCond = BindCond::BindNotAllowed; else if (cond == "conflict") - errCond = BindConflict; + errCond = BindCond::BindConflict; } } diff --git a/src/xmpp/xmpp-core/protocol.h b/src/xmpp/xmpp-core/protocol.h index 2db6352f..09c8a9d0 100644 --- a/src/xmpp/xmpp-core/protocol.h +++ b/src/xmpp/xmpp-core/protocol.h @@ -29,6 +29,8 @@ #include #include +#include + #define NS_ETHERX "http://etherx.jabber.org/streams" #define NS_CLIENT "jabber:client" #define NS_SERVER "jabber:server" @@ -70,20 +72,20 @@ class StreamFeatures { class BasicProtocol : public XmlProtocol { public: // xmpp 1.0 error conditions // rfc6120 - enum SASLCond { + enum class SASLCond { Aborted, // server confirms auth abort AccountDisabled, // account temporrily disabled CredentialsExpired, // credential expired EncryptionRequired, // can't use mech without TLS IncorrectEncoding, // Incorrect encoding. should not happen InvalidAuthzid, // bad input JID - InvalidMech, // bad mechanism + InvalidMechanism, // bad mechanism MalformedRequest, // malformded request - MechTooWeak, // can't use mech with this authzid + MechanismTooWeak, // can't use mech with this authzid NotAuthorized, // bad user, bad password, bad creditials TemporaryAuthFailure, // please try again later! }; - enum StreamCond { + enum class StreamCond { BadFormat, BadNamespacePrefix, Conflict, @@ -95,10 +97,11 @@ class BasicProtocol : public XmlProtocol { InvalidFrom, InvalidNamespace, InvalidXml, - StreamNotAuthorized, + NotAuthorized, + NotWellFormed, PolicyViolation, RemoteConnectionFailed, - StreamReset, + Reset, ResourceConstraint, RestrictedXml, SeeOtherHost, @@ -107,9 +110,8 @@ class BasicProtocol : public XmlProtocol { UnsupportedEncoding, UnsupportedStanzaType, UnsupportedVersion, - NotWellFormed }; - enum BindCond { BindBadRequest, BindNotAllowed, BindConflict }; + enum class BindCond { BindBadRequest, BindNotAllowed, BindConflict }; // extend the XmlProtocol enums enum Need { @@ -165,18 +167,19 @@ class BasicProtocol : public XmlProtocol { // shutdown void shutdown(); - void shutdownWithError(int cond, const QString &otherHost = ""); + void shutdownWithError(StreamCond cond, const QString &otherHost = ""); // information QString to, from, id, lang; Version version; // error output - int errCond; - QString errText; - QHash errLangText; - QDomElement errAppSpec; - QString otherHost; + using ErrorCond = std::variant; // int for old/deprecated error codes + std::optional errCond; + QString errText; + QHash errLangText; + QDomElement errAppSpec; + QString otherHost; QByteArray spare; // filled with unprocessed data on NStartTLS and NSASLLayer @@ -185,19 +188,19 @@ class BasicProtocol : public XmlProtocol { enum { TypeElement, TypeStanza, TypeDirect, TypePing }; protected: - static int stringToSASLCond(const QString &s); - static int stringToStreamCond(const QString &s); - static QString saslCondToString(int); - static QString streamCondToString(int); + static std::optional stringToSASLCond(const QString &s); + static std::optional stringToStreamCond(const QString &s); + static QString saslCondToString(SASLCond); + static QString streamCondToString(StreamCond); void send(const QDomElement &e, bool clip = false); void sendUrgent(const QDomElement &e, bool clip = false); - void sendStreamError(int cond, const QString &text = "", const QDomElement &appSpec = QDomElement()); + void sendStreamError(StreamCond cond, const QString &text = "", const QDomElement &appSpec = QDomElement()); void sendStreamError(const QString &text); // old-style - bool errorAndClose(int cond, const QString &text = "", const QDomElement &appSpec = QDomElement()); + bool errorAndClose(StreamCond cond, const QString &text = "", const QDomElement &appSpec = QDomElement()); bool error(int code); - void delayErrorAndClose(int cond, const QString &text = "", const QDomElement &appSpec = QDomElement()); + void delayErrorAndClose(StreamCond cond, const QString &text = "", const QDomElement &appSpec = QDomElement()); void delayError(int code); // reimplemented @@ -224,16 +227,16 @@ class BasicProtocol : public XmlProtocol { private: struct SASLCondEntry { - const char *str; - int cond; + QString str; + SASLCond cond; }; - static SASLCondEntry saslCondTable[]; + static std::array saslCondTable; struct StreamCondEntry { - const char *str; - int cond; + QString str; + StreamCond cond; }; - static StreamCondEntry streamCondTable[]; + static std::array streamCondTable; struct SendItem { QDomElement stanzaToSend; diff --git a/src/xmpp/xmpp-core/stream.cpp b/src/xmpp/xmpp-core/stream.cpp index 0384166c..14a8f180 100644 --- a/src/xmpp/xmpp-core/stream.cpp +++ b/src/xmpp/xmpp-core/stream.cpp @@ -834,7 +834,7 @@ void ClientStream::srvProcessNext() if (d->srv.to != d->server) { // host-gone, host-unknown, see-other-host - d->srv.shutdownWithError(CoreProtocol::HostUnknown); + d->srv.shutdownWithError(CoreProtocol::StreamCond::HostUnknown); } else d->srv.setFrom(d->server); break; @@ -1296,7 +1296,7 @@ void ClientStream::handleError() reset(); emit error(ErrProtocol); } else if (c == CoreProtocol::ErrStream) { - int x = d->client.errCond; + auto x = std::get(*d->client.errCond); QString text = d->client.errText; auto langText = d->client.errLangText; QDomElement appSpec = d->client.errAppSpec; @@ -1305,91 +1305,91 @@ void ClientStream::handleError() int strErr = -1; switch (x) { - case CoreProtocol::BadFormat: { + case CoreProtocol::StreamCond::BadFormat: { break; } // should NOT happen (we send the right format) - case CoreProtocol::BadNamespacePrefix: { + case CoreProtocol::StreamCond::BadNamespacePrefix: { break; } // should NOT happen (we send prefixes) - case CoreProtocol::Conflict: { + case CoreProtocol::StreamCond::Conflict: { strErr = Conflict; break; } - case CoreProtocol::ConnectionTimeout: { + case CoreProtocol::StreamCond::ConnectionTimeout: { strErr = ConnectionTimeout; break; } - case CoreProtocol::HostGone: { + case CoreProtocol::StreamCond::HostGone: { connErr = HostGone; break; } - case CoreProtocol::HostUnknown: { + case CoreProtocol::StreamCond::HostUnknown: { connErr = HostUnknown; break; } - case CoreProtocol::ImproperAddressing: { + case CoreProtocol::StreamCond::ImproperAddressing: { break; } // should NOT happen (we aren't a server) - case CoreProtocol::InternalServerError: { + case CoreProtocol::StreamCond::InternalServerError: { strErr = InternalServerError; break; } - case CoreProtocol::InvalidFrom: { + case CoreProtocol::StreamCond::InvalidFrom: { strErr = InvalidFrom; break; } - case CoreProtocol::InvalidNamespace: { + case CoreProtocol::StreamCond::InvalidNamespace: { break; } // should NOT happen (we set the right ns) - case CoreProtocol::InvalidXml: { + case CoreProtocol::StreamCond::InvalidXml: { strErr = InvalidXml; break; } // shouldn't happen either, but just in case ... - case CoreProtocol::StreamNotAuthorized: { + case CoreProtocol::StreamCond::NotAuthorized: { break; } // should NOT happen (we're not stupid) - case CoreProtocol::PolicyViolation: { + case CoreProtocol::StreamCond::PolicyViolation: { strErr = PolicyViolation; break; } - case CoreProtocol::RemoteConnectionFailed: { + case CoreProtocol::StreamCond::RemoteConnectionFailed: { connErr = RemoteConnectionFailed; break; } - case CoreProtocol::StreamReset: { + case CoreProtocol::StreamCond::Reset: { strErr = StreamReset; break; } - case CoreProtocol::ResourceConstraint: { + case CoreProtocol::StreamCond::ResourceConstraint: { strErr = ResourceConstraint; break; } - case CoreProtocol::RestrictedXml: { + case CoreProtocol::StreamCond::RestrictedXml: { strErr = InvalidXml; break; } // group with this one - case CoreProtocol::SeeOtherHost: { + case CoreProtocol::StreamCond::SeeOtherHost: { connErr = SeeOtherHost; break; } - case CoreProtocol::SystemShutdown: { + case CoreProtocol::StreamCond::SystemShutdown: { strErr = SystemShutdown; break; } - case CoreProtocol::UndefinedCondition: { + case CoreProtocol::StreamCond::UndefinedCondition: { break; } // leave as null error - case CoreProtocol::UnsupportedEncoding: { + case CoreProtocol::StreamCond::UnsupportedEncoding: { break; } // should NOT happen (we send good encoding) - case CoreProtocol::UnsupportedStanzaType: { + case CoreProtocol::StreamCond::UnsupportedStanzaType: { break; } // should NOT happen (we're not stupid) - case CoreProtocol::UnsupportedVersion: { + case CoreProtocol::StreamCond::UnsupportedVersion: { connErr = UnsupportedVersion; break; } - case CoreProtocol::NotWellFormed: { + case CoreProtocol::StreamCond::NotWellFormed: { strErr = InvalidXml; break; } // group with this one @@ -1418,9 +1418,9 @@ void ClientStream::handleError() d->errCond = TLSStart; emit error(ErrTLS); } else if (c == CoreProtocol::ErrAuth) { - int x = d->client.errCond; int r = GenericAuthError; if (d->client.old) { + auto x = std::get(*d->client.errCond); if (x == 401) // not authorized r = NotAuthorized; else if (x == 409) // conflict @@ -1428,48 +1428,49 @@ void ClientStream::handleError() else if (x == 406) // not acceptable (this should NOT happen) r = GenericAuthError; } else { + auto x = std::get(*d->client.errCond); switch (x) { - case CoreProtocol::Aborted: { + case CoreProtocol::SASLCond::Aborted: { r = GenericAuthError; break; } // should NOT happen (we never send ) - case CoreProtocol::AccountDisabled: { + case CoreProtocol::SASLCond::AccountDisabled: { r = AccountDisabled; break; } // account temporrily disabled - case CoreProtocol::CredentialsExpired: { + case CoreProtocol::SASLCond::CredentialsExpired: { r = CredentialsExpired; break; } // credential expired - case CoreProtocol::EncryptionRequired: { + case CoreProtocol::SASLCond::EncryptionRequired: { r = EncryptionRequired; break; } // can't use mech without TLS - case CoreProtocol::IncorrectEncoding: { + case CoreProtocol::SASLCond::IncorrectEncoding: { r = GenericAuthError; break; } // should NOT happen - case CoreProtocol::InvalidAuthzid: { + case CoreProtocol::SASLCond::InvalidAuthzid: { r = InvalidAuthzid; break; } - case CoreProtocol::InvalidMech: { + case CoreProtocol::SASLCond::InvalidMechanism: { r = InvalidMech; break; } - case CoreProtocol::MalformedRequest: { + case CoreProtocol::SASLCond::MalformedRequest: { r = MalformedRequest; break; } - case CoreProtocol::MechTooWeak: { + case CoreProtocol::SASLCond::MechanismTooWeak: { r = MechTooWeak; break; } - case CoreProtocol::NotAuthorized: { + case CoreProtocol::SASLCond::NotAuthorized: { r = NotAuthorized; break; } - case CoreProtocol::TemporaryAuthFailure: { + case CoreProtocol::SASLCond::TemporaryAuthFailure: { r = TemporaryAuthFailure; break; } @@ -1484,12 +1485,13 @@ void ClientStream::handleError() d->errCond = NoMech; emit error(ErrAuth); } else if (c == CoreProtocol::ErrBind) { - int r = -1; - if (d->client.errCond == CoreProtocol::BindBadRequest) { + int r = -1; + auto x = std::get(*d->client.errCond); + if (x == CoreProtocol::BindCond::BindBadRequest) { // should NOT happen - } else if (d->client.errCond == CoreProtocol::BindNotAllowed) { + } else if (x == CoreProtocol::BindCond::BindNotAllowed) { r = BindNotAllowed; - } else if (d->client.errCond == CoreProtocol::BindConflict) { + } else if (x == CoreProtocol::BindCond::BindConflict) { r = BindConflict; } diff --git a/src/xmpp/xmpp-core/xmlprotocol.cpp b/src/xmpp/xmpp-core/xmlprotocol.cpp index ab89fed4..405d92af 100644 --- a/src/xmpp/xmpp-core/xmlprotocol.cpp +++ b/src/xmpp/xmpp-core/xmlprotocol.cpp @@ -97,6 +97,16 @@ static QString xmlToString(const QDomElement &e, const QString &fakeNS, const QS QString out; { QTextStream ts(&out, QIODevice::WriteOnly); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + // NOTE: Workaround for bug in QtXML https://bugreports.qt.io/browse/QTBUG-25291 (Qt4 and Qt5 MinGW only): + // Qt by default convert low surrogate to XML notation &#x....; and let high in binary! + // + // Qt is calling encode function per UTF-16 codepoint, which means that high and low + // surrogate pairs are encoded separately. So all encoding except UTF-16 will leads + // to damaged Unicode characters above 0xFFFF. Internal QString encoding is UTF-16 + // so this should be safe as QString still contains valid Unicode characters. + ts.setCodec("UTF-16"); +#endif fake.firstChild().save(ts, 0); } // 'clip' means to remove any unwanted (and unneeded) characters, such as a trailing newline @@ -127,6 +137,9 @@ static void createRootXmlTags(const QDomElement &root, QString *xmlHeader, QStri QString str; { QTextStream ts(&str, QIODevice::WriteOnly); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + ts.setCodec("UTF-16"); +#endif e.save(ts, 0); } diff --git a/src/xmpp/xmpp-core/xmpp.h b/src/xmpp/xmpp-core/xmpp.h index 70e058fb..f6689ce8 100644 --- a/src/xmpp/xmpp-core/xmpp.h +++ b/src/xmpp/xmpp-core/xmpp.h @@ -31,6 +31,8 @@ #include #include // For QCA::SASL::Params +#include + #ifndef CS_XMPP class ByteStream; #endif @@ -76,7 +78,6 @@ class Connector : public QObject { quint16 peerPort() const; virtual QString host() const; - signals: void connected(); void error(); @@ -85,6 +86,7 @@ class Connector : public QObject { void setUseSSL(bool b); void setPeerAddressNone(); void setPeerAddress(const QHostAddress &addr, quint16 port); + void setRequireDirectTLS(bool require); private: bool ssl; // a flag to start ssl handshake immediately @@ -133,8 +135,8 @@ class AdvancedConnector : public Connector { }; void setProxy(const Proxy &proxy); - void setOptProbe(bool); void setOptSSL(bool); + void setOptTlsSrv(bool); void changePollInterval(int secs); @@ -162,7 +164,7 @@ private slots: private: class Private; - Private *d; + std::unique_ptr d; void cleanup(); }; diff --git a/src/xmpp/xmpp-core/xmpp_stanza.cpp b/src/xmpp/xmpp-core/xmpp_stanza.cpp index b3779539..aefe2588 100644 --- a/src/xmpp/xmpp-core/xmpp_stanza.cpp +++ b/src/xmpp/xmpp-core/xmpp_stanza.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include + using namespace XMPP; #define NS_STANZAS "urn:ietf:params:xml:ns:xmpp-stanzas" @@ -72,7 +75,7 @@ using namespace XMPP; /** \brief Constructs new error */ -Stanza::Error::Error(int _type, int _condition, const QString &_text, const QDomElement &_appSpec) +Stanza::Error::Error(ErrorType _type, ErrorCond _condition, const QString &_text, const QDomElement &_appSpec) { type = _type; condition = _condition; @@ -81,222 +84,218 @@ Stanza::Error::Error(int _type, int _condition, const QString &_text, const QDom originalCode = 0; } -void Stanza::Error::reset() -{ - type = 0; - condition = UndefinedCondition; - text.clear(); - by.clear(); - appSpec = QDomElement(); - originalCode = 0; -} - class Stanza::Error::Private { public: struct ErrorTypeEntry { - const char *str; - int type; + QString str; + ErrorType type; }; - static ErrorTypeEntry errorTypeTable[]; + static std::array errorTypeTable; struct ErrorCondEntry { - const char *str; - int cond; + QString str; + ErrorCond cond; }; - static ErrorCondEntry errorCondTable[]; + static std::array errorCondTable; struct ErrorCodeEntry { - int cond; - int type; - int code; + ErrorCond cond; + ErrorType type; + int code; }; - static ErrorCodeEntry errorCodeTable[]; + static std::array errorCodeTable; struct ErrorDescEntry { - int cond; + ErrorCond cond; const char *name; const char *str; }; - static ErrorDescEntry errorDescriptions[]; + static std::array errorDescriptions; - static int stringToErrorType(const QString &s) + static std::optional stringToErrorType(const QString &s) { - for (int n = 0; errorTypeTable[n].str; ++n) { - if (s == errorTypeTable[n].str) - return errorTypeTable[n].type; + for (auto const &entry : errorTypeTable) { + if (s == entry.str) + return entry.type; } - return -1; + return {}; } - static QString errorTypeToString(int x) + static QString errorTypeToString(ErrorType x) { - for (int n = 0; errorTypeTable[n].str; ++n) { - if (x == errorTypeTable[n].type) - return errorTypeTable[n].str; + for (auto const &entry : errorTypeTable) { + if (x == entry.type) + return entry.str; } - return QString(); + return {}; } - static int stringToErrorCond(const QString &s) + static std::optional stringToErrorCond(const QString &s) { - for (int n = 0; errorCondTable[n].str; ++n) { - if (s == errorCondTable[n].str) - return errorCondTable[n].cond; + for (auto const &entry : errorCondTable) { + if (s == entry.str) + return entry.cond; } - return -1; + return {}; } - static QString errorCondToString(int x) + static QString errorCondToString(ErrorCond x) { - for (int n = 0; errorCondTable[n].str; ++n) { - if (x == errorCondTable[n].cond) - return errorCondTable[n].str; + for (auto const &entry : errorCondTable) { + if (x == entry.cond) + return entry.str; } return QString(); } - static int errorTypeCondToCode(int t, int c) + static int errorTypeCondToCode(ErrorType t, ErrorCond c) { Q_UNUSED(t); - for (int n = 0; errorCodeTable[n].cond; ++n) { - if (c == errorCodeTable[n].cond) - return errorCodeTable[n].code; + for (auto const &entry : errorCodeTable) { + if (c == entry.cond) + return entry.code; } return 0; } - static QPair errorCodeToTypeCond(int x) + static std::optional> errorCodeToTypeCond(int x) { - for (int n = 0; errorCodeTable[n].cond; ++n) { - if (x == errorCodeTable[n].code) - return QPair(errorCodeTable[n].type, errorCodeTable[n].cond); + for (auto const &entry : errorCodeTable) { + if (x == entry.code) + return std::make_pair(entry.type, entry.cond); } - return QPair(-1, -1); + return {}; } - static QPair errorCondToDesc(int x) + static QPair errorCondToDesc(ErrorCond x) { - for (int n = 0; errorDescriptions[n].str; ++n) { - if (x == errorDescriptions[n].cond) - return QPair( - QCoreApplication::translate("Stanza::Error::Private", errorDescriptions[n].name), - QCoreApplication::translate("Stanza::Error::Private", errorDescriptions[n].str)); + for (auto const &entry : errorDescriptions) { + if (x == entry.cond) + return QPair(QCoreApplication::translate("Stanza::Error::Private", entry.name), + QCoreApplication::translate("Stanza::Error::Private", entry.str)); } return QPair(); } }; -Stanza::Error::Private::ErrorTypeEntry Stanza::Error::Private::errorTypeTable[] = { - { "cancel", Cancel }, { "continue", Continue }, { "modify", Modify }, - { "auth", Auth }, { "wait", Wait }, { nullptr, 0 }, -}; - -Stanza::Error::Private::ErrorCondEntry Stanza::Error::Private::errorCondTable[] = { - { "bad-request", BadRequest }, - { "conflict", Conflict }, - { "feature-not-implemented", FeatureNotImplemented }, - { "forbidden", Forbidden }, - { "gone", Gone }, - { "internal-server-error", InternalServerError }, - { "item-not-found", ItemNotFound }, - { "jid-malformed", JidMalformed }, - { "not-acceptable", NotAcceptable }, - { "not-allowed", NotAllowed }, - { "not-authorized", NotAuthorized }, - { "recipient-unavailable", RecipientUnavailable }, - { "redirect", Redirect }, - { "registration-required", RegistrationRequired }, - { "remote-server-not-found", RemoteServerNotFound }, - { "remote-server-timeout", RemoteServerTimeout }, - { "resource-constraint", ResourceConstraint }, - { "service-unavailable", ServiceUnavailable }, - { "subscription-required", SubscriptionRequired }, - { "undefined-condition", UndefinedCondition }, - { "unexpected-request", UnexpectedRequest }, - { nullptr, 0 }, +std::array Stanza::Error::Private::errorTypeTable { + { { QStringLiteral("cancel"), ErrorType::Cancel }, + { QStringLiteral("continue"), ErrorType::Continue }, + { QStringLiteral("modify"), ErrorType::Modify }, + { QStringLiteral("auth"), ErrorType::Auth }, + { QStringLiteral("wait"), ErrorType::Wait } } }; -Stanza::Error::Private::ErrorCodeEntry Stanza::Error::Private::errorCodeTable[] = { - { BadRequest, Modify, 400 }, - { Conflict, Cancel, 409 }, - { FeatureNotImplemented, Cancel, 501 }, - { Forbidden, Auth, 403 }, - { Gone, Modify, 302 }, // permanent - { InternalServerError, Wait, 500 }, - { ItemNotFound, Cancel, 404 }, - { JidMalformed, Modify, 400 }, - { NotAcceptable, Modify, 406 }, - { NotAllowed, Cancel, 405 }, - { NotAuthorized, Auth, 401 }, - { RecipientUnavailable, Wait, 404 }, - { Redirect, Modify, 302 }, // temporary - { RegistrationRequired, Auth, 407 }, - { RemoteServerNotFound, Cancel, 404 }, - { RemoteServerTimeout, Wait, 504 }, - { ResourceConstraint, Wait, 500 }, - { ServiceUnavailable, Cancel, 503 }, - { SubscriptionRequired, Auth, 407 }, - { UndefinedCondition, Wait, 500 }, // Note: any type matches really - { UnexpectedRequest, Wait, 400 }, - { 0, 0, 0 }, -}; - -Stanza::Error::Private::ErrorDescEntry Stanza::Error::Private::errorDescriptions[] = { - { BadRequest, QT_TR_NOOP("Bad request"), +std::array Stanza::Error::Private::errorCondTable { { + { QStringLiteral("bad-request"), ErrorCond::BadRequest }, + { QStringLiteral("conflict"), ErrorCond::Conflict }, + { QStringLiteral("feature-not-implemented"), ErrorCond::FeatureNotImplemented }, + { QStringLiteral("forbidden"), ErrorCond::Forbidden }, + { QStringLiteral("gone"), ErrorCond::Gone }, + { QStringLiteral("internal-server-error"), ErrorCond::InternalServerError }, + { QStringLiteral("item-not-found"), ErrorCond::ItemNotFound }, + { QStringLiteral("jid-malformed"), ErrorCond::JidMalformed }, + { QStringLiteral("not-acceptable"), ErrorCond::NotAcceptable }, + { QStringLiteral("not-allowed"), ErrorCond::NotAllowed }, + { QStringLiteral("not-authorized"), ErrorCond::NotAuthorized }, + { QStringLiteral("policy-violation"), ErrorCond::PolicyViolation }, + { QStringLiteral("recipient-unavailable"), ErrorCond::RecipientUnavailable }, + { QStringLiteral("redirect"), ErrorCond::Redirect }, + { QStringLiteral("registration-required"), ErrorCond::RegistrationRequired }, + { QStringLiteral("remote-server-not-found"), ErrorCond::RemoteServerNotFound }, + { QStringLiteral("remote-server-timeout"), ErrorCond::RemoteServerTimeout }, + { QStringLiteral("resource-constraint"), ErrorCond::ResourceConstraint }, + { QStringLiteral("service-unavailable"), ErrorCond::ServiceUnavailable }, + { QStringLiteral("subscription-required"), ErrorCond::SubscriptionRequired }, + { QStringLiteral("undefined-condition"), ErrorCond::UndefinedCondition }, + { QStringLiteral("unexpected-request"), ErrorCond::UnexpectedRequest }, +} }; + +std::array Stanza::Error::Private::errorCodeTable { { + { ErrorCond::BadRequest, ErrorType::Modify, 400 }, + { ErrorCond::Conflict, ErrorType::Cancel, 409 }, + { ErrorCond::FeatureNotImplemented, ErrorType::Cancel, 501 }, + { ErrorCond::Forbidden, ErrorType::Auth, 403 }, + { ErrorCond::Gone, ErrorType::Modify, 302 }, // permanent + { ErrorCond::InternalServerError, ErrorType::Wait, 500 }, + { ErrorCond::ItemNotFound, ErrorType::Cancel, 404 }, + { ErrorCond::JidMalformed, ErrorType::Modify, 400 }, + { ErrorCond::NotAcceptable, ErrorType::Modify, 406 }, + { ErrorCond::NotAllowed, ErrorType::Cancel, 405 }, + { ErrorCond::NotAuthorized, ErrorType::Auth, 401 }, + { ErrorCond::PolicyViolation, ErrorType::Modify, 402 }, // it can be Wait too according to rfc6120 + { ErrorCond::RecipientUnavailable, ErrorType::Wait, 404 }, + { ErrorCond::Redirect, ErrorType::Modify, 302 }, // temporary + { ErrorCond::RegistrationRequired, ErrorType::Auth, 407 }, + { ErrorCond::RemoteServerNotFound, ErrorType::Cancel, 404 }, + { ErrorCond::RemoteServerTimeout, ErrorType::Wait, 504 }, + { ErrorCond::ResourceConstraint, ErrorType::Wait, 500 }, + { ErrorCond::ServiceUnavailable, ErrorType::Cancel, 503 }, + { ErrorCond::SubscriptionRequired, ErrorType::Auth, 407 }, + { ErrorCond::UndefinedCondition, ErrorType::Wait, 500 }, // Note: any type matches really + { ErrorCond::UnexpectedRequest, ErrorType::Wait, 400 }, +} }; + +std::array Stanza::Error::Private::errorDescriptions { { + { ErrorCond::BadRequest, QT_TR_NOOP("Bad request"), QT_TR_NOOP("The sender has sent XML that is malformed or that cannot be processed.") }, - { Conflict, QT_TR_NOOP("Conflict"), + { ErrorCond::Conflict, QT_TR_NOOP("Conflict"), QT_TR_NOOP( "Access cannot be granted because an existing resource or session exists with the same name or address.") }, - { FeatureNotImplemented, QT_TR_NOOP("Feature not implemented"), + { ErrorCond::FeatureNotImplemented, QT_TR_NOOP("Feature not implemented"), QT_TR_NOOP( "The feature requested is not implemented by the recipient or server and therefore cannot be processed.") }, - { Forbidden, QT_TR_NOOP("Forbidden"), + { ErrorCond::Forbidden, QT_TR_NOOP("Forbidden"), QT_TR_NOOP("The requesting entity does not possess the required permissions to perform the action.") }, - { Gone, QT_TR_NOOP("Gone"), QT_TR_NOOP("The recipient or server can no longer be contacted at this address.") }, - { InternalServerError, QT_TR_NOOP("Internal server error"), + { ErrorCond::Gone, QT_TR_NOOP("Gone"), + QT_TR_NOOP("The recipient or server can no longer be contacted at this address.") }, + { ErrorCond::InternalServerError, QT_TR_NOOP("Internal server error"), QT_TR_NOOP("The server could not process the stanza because of a misconfiguration or an otherwise-undefined " "internal server error.") }, - { ItemNotFound, QT_TR_NOOP("Item not found"), QT_TR_NOOP("The addressed JID or item requested cannot be found.") }, - { JidMalformed, QT_TR_NOOP("JID malformed"), + { ErrorCond::ItemNotFound, QT_TR_NOOP("Item not found"), + QT_TR_NOOP("The addressed JID or item requested cannot be found.") }, + { ErrorCond::JidMalformed, QT_TR_NOOP("JID malformed"), QT_TR_NOOP("The sending entity has provided or communicated an XMPP address (e.g., a value of the 'to' " "attribute) or aspect thereof (e.g., a resource identifier) that does not adhere to the syntax " "defined in Addressing Scheme.") }, - { NotAcceptable, QT_TR_NOOP("Not acceptable"), + { ErrorCond::NotAcceptable, QT_TR_NOOP("Not acceptable"), QT_TR_NOOP("The recipient or server understands the request but is refusing to process it because it does not " "meet criteria defined by the recipient or server (e.g., a local policy regarding acceptable words in " "messages).") }, - { NotAllowed, QT_TR_NOOP("Not allowed"), + { ErrorCond::NotAllowed, QT_TR_NOOP("Not allowed"), QT_TR_NOOP("The recipient or server does not allow any entity to perform the action.") }, - { NotAuthorized, QT_TR_NOOP("Not authorized"), + { ErrorCond::NotAuthorized, QT_TR_NOOP("Not authorized"), QT_TR_NOOP("The sender must provide proper credentials before being allowed to perform the action, or has " "provided improper credentials.") }, - { RecipientUnavailable, QT_TR_NOOP("Recipient unavailable"), + { ErrorCond::PolicyViolation, QT_TR_NOOP("Policy violation"), + QT_TR_NOOP("The sender has violated some service policy.") }, + { ErrorCond::RecipientUnavailable, QT_TR_NOOP("Recipient unavailable"), QT_TR_NOOP("The intended recipient is temporarily unavailable.") }, - { Redirect, QT_TR_NOOP("Redirect"), + { ErrorCond::Redirect, QT_TR_NOOP("Redirect"), QT_TR_NOOP("The recipient or server is redirecting requests for this information to another entity, usually " "temporarily.") }, - { RegistrationRequired, QT_TR_NOOP("Registration required"), + { ErrorCond::RegistrationRequired, QT_TR_NOOP("Registration required"), QT_TR_NOOP("The requesting entity is not authorized to access the requested service because registration is " "required.") }, - { RemoteServerNotFound, QT_TR_NOOP("Remote server not found"), + { ErrorCond::RemoteServerNotFound, QT_TR_NOOP("Remote server not found"), QT_TR_NOOP( "A remote server or service specified as part or all of the JID of the intended recipient does not exist.") }, - { RemoteServerTimeout, QT_TR_NOOP("Remote server timeout"), + { ErrorCond::RemoteServerTimeout, QT_TR_NOOP("Remote server timeout"), QT_TR_NOOP("A remote server or service specified as part or all of the JID of the intended recipient (or " "required to fulfill a request) could not be contacted within a reasonable amount of time.") }, - { ResourceConstraint, QT_TR_NOOP("Resource constraint"), + { ErrorCond::ResourceConstraint, QT_TR_NOOP("Resource constraint"), QT_TR_NOOP("The server or recipient lacks the system resources necessary to service the request.") }, - { ServiceUnavailable, QT_TR_NOOP("Service unavailable"), + { ErrorCond::ServiceUnavailable, QT_TR_NOOP("Service unavailable"), QT_TR_NOOP("The server or recipient does not currently provide the requested service.") }, - { SubscriptionRequired, QT_TR_NOOP("Subscription required"), + { ErrorCond::SubscriptionRequired, QT_TR_NOOP("Subscription required"), QT_TR_NOOP("The requesting entity is not authorized to access the requested service because a subscription is " "required.") }, - { UndefinedCondition, QT_TR_NOOP("Undefined condition"), + { ErrorCond::UndefinedCondition, QT_TR_NOOP("Undefined condition"), QT_TR_NOOP("The error condition is not one of those defined by the other conditions in this list.") }, - { UnexpectedRequest, QT_TR_NOOP("Unexpected request"), + { ErrorCond::UnexpectedRequest, QT_TR_NOOP("Unexpected request"), QT_TR_NOOP("The recipient or server understood the request but was not expecting it at this time (e.g., the " "request was out of order).") }, -}; +} }; /** \brief Returns the error code @@ -316,12 +315,12 @@ int Stanza::Error::code() const { return originalCode ? originalCode : Private:: */ bool Stanza::Error::fromCode(int code) { - QPair guess = Private::errorCodeToTypeCond(code); - if (guess.first == -1 || guess.second == -1) + auto guess = Private::errorCodeToTypeCond(code); + if (!guess.has_value()) return false; - type = guess.first; - condition = guess.second; + type = guess->first; + condition = guess->second; originalCode = code; return true; @@ -341,40 +340,52 @@ bool Stanza::Error::fromXml(const QDomElement &e, const QString &baseNS) return false; // type - type = Private::stringToErrorType(e.attribute("type")); - by = e.attribute(QLatin1String("by")); - condition = -1; + auto parsedType = Private::stringToErrorType(e.attribute("type")); + if (!parsedType.has_value()) { + // code. deprecated. rfc6120 has just a little note about it. also see XEP-0086 + bool ok; + originalCode = e.attribute("code").toInt(&ok); + if (ok && originalCode) { + auto guess = Private::errorCodeToTypeCond(originalCode); + if (guess.has_value()) { + type = guess->first; + condition = guess->second; + } else { + ok = false; + } + } + if (!ok) { + qWarning("unexpected error type=%s", qUtf8Printable(e.attribute("type"))); + return false; + } + } else { + type = *parsedType; + condition = ErrorCond(-1); + } - QString textTag(QString::fromLatin1("text")); + by = e.attribute(QStringLiteral("by")); + QString textTag(QStringLiteral("text")); for (auto t = e.firstChildElement(); !t.isNull(); t = t.nextSiblingElement()) { if (t.namespaceURI() == NS_STANZAS) { if (t.tagName() == textTag) { text = t.text().trimmed(); } else { - condition = Private::stringToErrorCond(t.tagName()); + auto parsedCond = Private::stringToErrorCond(t.tagName()); + if (parsedCond.has_value()) { + condition = *parsedCond; + } } } else { appSpec = t; } - if (condition != -1 && !appSpec.isNull() && !text.isEmpty()) + if (condition != ErrorCond(-1) && !appSpec.isNull() && !text.isEmpty()) break; } - // code - originalCode - = e.attribute("code").toInt(); // deprecated. rfc6120 has just a little note about it. also see XEP-0086 - // try to guess type/condition - if (type == -1 || condition == -1) { - QPair guess(-1, -1); - if (originalCode) - guess = Private::errorCodeToTypeCond(originalCode); - - if (type == -1) - type = guess.first != -1 ? guess.first : Cancel; - if (condition == -1) - condition = guess.second != -1 ? guess.second : UndefinedCondition; + if (condition == ErrorCond(-1)) { + condition = ErrorCond::UndefinedCondition; } return true; @@ -453,11 +464,11 @@ class Stanza::Private { public: static int stringToKind(const QString &s) { - if (s == QLatin1String("message")) + if (s == QStringLiteral("message")) return Message; - else if (s == QLatin1String("presence")) + else if (s == QStringLiteral("presence")) return Presence; - else if (s == QLatin1String("iq")) + else if (s == QStringLiteral("iq")) return IQ; else return -1; @@ -466,11 +477,11 @@ class Stanza::Private { static QString kindToString(Kind k) { if (k == Message) - return QLatin1String("message"); + return QStringLiteral("message"); else if (k == Presence) - return QLatin1String("presence"); + return QStringLiteral("presence"); else - return QLatin1String("iq"); + return QStringLiteral("iq"); } Stream *s; diff --git a/src/xmpp/xmpp-core/xmpp_stanza.h b/src/xmpp/xmpp-core/xmpp_stanza.h index c562f58c..234ecb79 100644 --- a/src/xmpp/xmpp-core/xmpp_stanza.h +++ b/src/xmpp/xmpp-core/xmpp_stanza.h @@ -41,8 +41,8 @@ class Stanza { class Error { public: - enum ErrorType { Cancel = 1, Continue, Modify, Auth, Wait }; - enum ErrorCond { + enum class ErrorType { Cancel = 1, Continue, Modify, Auth, Wait }; + enum class ErrorCond { BadRequest = 1, Conflict, FeatureNotImplemented, @@ -54,7 +54,7 @@ class Stanza { NotAcceptable, NotAllowed, NotAuthorized, - PaymentRequired, + PolicyViolation, RecipientUnavailable, Redirect, RegistrationRequired, @@ -67,20 +67,24 @@ class Stanza { UnexpectedRequest }; - Error(int type = Cancel, int condition = UndefinedCondition, const QString &text = QString(), - const QDomElement &appSpec = QDomElement()); + Error(ErrorType type = ErrorType::Cancel, ErrorCond condition = ErrorCond::UndefinedCondition, + const QString &text = QString(), const QDomElement &appSpec = QDomElement()); - int type; - int condition; + ErrorType type; + ErrorCond condition; QString text; QString by; QDomElement appSpec; - void reset(); - int code() const; bool fromCode(int code); + inline bool isCancel() const { return type == ErrorType::Cancel; } + inline bool isContinue() const { return type == ErrorType::Continue; } + inline bool isModify() const { return type == ErrorType::Modify; } + inline bool isAuth() const { return type == ErrorType::Auth; } + inline bool isWait() const { return type == ErrorType::Wait; } + QPair description() const; QString toString() const; diff --git a/src/xmpp/xmpp-im/client.cpp b/src/xmpp/xmpp-im/client.cpp index 2761ff91..36b4a84e 100644 --- a/src/xmpp/xmpp-im/client.cpp +++ b/src/xmpp/xmpp-im/client.cpp @@ -290,7 +290,7 @@ void Client::setCapsOptimizationAllowed(bool allowed) { d->capsOptimization = al bool Client::capsOptimizationAllowed() const { - if (d->capsOptimization && d->active && d->serverInfoManager->features().hasCapsOptimize()) { + if (d->capsOptimization && d->active && d->serverInfoManager->serverFeatures().hasCapsOptimize()) { auto it = d->resourceList.find(d->resource); return it != d->resourceList.end() && it->status().isAvailable(); } @@ -533,7 +533,7 @@ void Client::streamReadyRead() debug(QString("Client: incoming: [\n%1]\n").arg(out)); emit xmlIncoming(out); - QDomElement x = s.element(); // oldStyleNS(s.element()); + QDomElement x = s.element(); distribute(x); } } @@ -870,7 +870,7 @@ void Client::pmMessage(const Message &m) d->ibbman->takeIncomingData(m.from(), m.id(), m.ibbData(), Stanza::Message); } - if (m.type() == "groupchat") { + if (m.type() == Message::Type::Groupchat) { for (QList::Iterator it = d->groupChatList.begin(); it != d->groupChatList.end(); it++) { const GroupChat &i = *it; diff --git a/src/xmpp/xmpp-im/filetransfer.cpp b/src/xmpp/xmpp-im/filetransfer.cpp index 714d6e22..837c8eca 100644 --- a/src/xmpp/xmpp-im/filetransfer.cpp +++ b/src/xmpp/xmpp-im/filetransfer.cpp @@ -414,7 +414,7 @@ void FileTransferManager::pft_incoming(const FTRequest &req) } if (streamType.isEmpty()) { - d->pft->respondError(req.from, req.iq_id, Stanza::Error::NotAcceptable, "No valid stream types"); + d->pft->respondError(req.from, req.iq_id, Stanza::Error::ErrorCond::NotAcceptable, "No valid stream types"); return; } @@ -485,7 +485,7 @@ void FileTransferManager::con_accept(FileTransfer *ft) void FileTransferManager::con_reject(FileTransfer *ft) { - d->pft->respondError(ft->d->peer, ft->d->iq_id, Stanza::Error::Forbidden, "Declined"); + d->pft->respondError(ft->d->peer, ft->d->iq_id, Stanza::Error::ErrorCond::Forbidden, "Declined"); } void FileTransferManager::unlink(FileTransfer *ft) { d->list.removeAll(ft); } @@ -691,7 +691,7 @@ void JT_PushFT::respondSuccess(const Jid &to, const QString &id, qlonglong range void JT_PushFT::respondError(const Jid &to, const QString &id, Stanza::Error::ErrorCond cond, const QString &str) { QDomElement iq = createIQ(doc(), "error", to.full(), id); - Stanza::Error error(Stanza::Error::Cancel, cond, str); + Stanza::Error error(Stanza::Error::ErrorType::Cancel, cond, str); iq.appendChild(error.toXml(*client()->doc(), client()->stream().baseNS())); send(iq); } @@ -719,7 +719,7 @@ bool JT_PushFT::take(const QDomElement &e) QString fname = file.attribute("name"); if (fname.isEmpty()) { - respondError(from, id, Stanza::Error::BadRequest, "Bad file name"); + respondError(from, id, Stanza::Error::ErrorCond::BadRequest, "Bad file name"); return true; } @@ -732,7 +732,7 @@ bool JT_PushFT::take(const QDomElement &e) bool ok; qlonglong size = file.attribute("size").toLongLong(&ok); if (!ok || size < 0) { - respondError(from, id, Stanza::Error::BadRequest, "Bad file size"); + respondError(from, id, Stanza::Error::ErrorCond::BadRequest, "Bad file size"); return true; } diff --git a/src/xmpp/xmpp-im/httpfileupload.cpp b/src/xmpp/xmpp-im/httpfileupload.cpp index efbdc696..4b7eed01 100644 --- a/src/xmpp/xmpp-im/httpfileupload.cpp +++ b/src/xmpp/xmpp-im/httpfileupload.cpp @@ -21,13 +21,13 @@ #include "xmpp_client.h" #include "xmpp_serverinfomanager.h" -#include "xmpp_tasks.h" #include "xmpp_xmlcommon.h" #include #include #include #include +#include using namespace XMPP; @@ -99,70 +99,69 @@ void HttpFileUpload::start() if (featureOptions.isEmpty()) { featureOptions << (QSet() << xmlns_v0_2_5) << (QSet() << xmlns_v0_3_1); } - d->client->serverInfoManager()->queryServiceInfo( + auto query = d->client->serverInfoManager()->queryServiceInfo( QLatin1String("store"), QLatin1String("file"), featureOptions, - QRegularExpression("^(upload|http|stor|file|dis|drive).*"), ServerInfoManager::SQ_CheckAllOnNoMatch, - [this](const QList &items) { - d->httpHosts.clear(); - for (const auto &item : items) { - const QStringList &l = item.features().list(); - XEP0363::version ver = XEP0363::vUnknown; - QString xmlns; - quint64 sizeLimit = 0; - if (l.contains(xmlns_v0_3_1)) { - ver = XEP0363::v0_3_1; - xmlns = xmlns_v0_3_1; - } else if (l.contains(xmlns_v0_2_5)) { - ver = XEP0363::v0_2_5; - xmlns = xmlns_v0_2_5; + QRegularExpression("^(upload|http|stor|file|dis|drive).*"), ServiceInfoQuery::CheckAllOnNoMatch); + connect(query, &ServiceInfoQuery::finished, this, [this](const QList &items) { + d->httpHosts.clear(); + for (const auto &item : items) { + const QStringList &l = item.features().list(); + XEP0363::version ver = XEP0363::vUnknown; + QString xmlns; + quint64 sizeLimit = 0; + if (l.contains(xmlns_v0_3_1)) { + ver = XEP0363::v0_3_1; + xmlns = xmlns_v0_3_1; + } else if (l.contains(xmlns_v0_2_5)) { + ver = XEP0363::v0_2_5; + xmlns = xmlns_v0_2_5; + } + if (ver != XEP0363::vUnknown) { + QVector> hosts; + const XData::Field field = item.registeredExtension(xmlns).getField(QLatin1String("max-file-size")); + if (field.isValid() && field.type() == XData::Field::Field_TextSingle) + sizeLimit = field.value().at(0).toULongLong(); + HttpHost host; + host.ver = ver; + host.jid = item.jid(); + host.sizeLimit = sizeLimit; + QVariant metaProps(d->client->serverInfoManager()->serviceMeta(host.jid, "httpprops")); + if (metaProps.isValid()) { + host.props = HostProps(metaProps.value()); + } else { + host.props = SecureGet | SecurePut; + if (ver == XEP0363::v0_3_1) + host.props |= NewestVer; } - if (ver != XEP0363::vUnknown) { - QVector> hosts; - const XData::Field field = item.registeredExtension(xmlns).getField(QLatin1String("max-file-size")); - if (field.isValid() && field.type() == XData::Field::Field_TextSingle) - sizeLimit = field.value().at(0).toULongLong(); - HttpHost host; - host.ver = ver; - host.jid = item.jid(); - host.sizeLimit = sizeLimit; - QVariant metaProps(d->client->serverInfoManager()->serviceMeta(host.jid, "httpprops")); - if (metaProps.isValid()) { - host.props = HostProps(metaProps.value()); - } else { - host.props = SecureGet | SecurePut; - if (ver == XEP0363::v0_3_1) - host.props |= NewestVer; - } - int value = 0; - if (host.props & SecureGet) - value += 5; - if (host.props & SecurePut) - value += 5; - if (host.props & NewestVer) - value += 3; - if (host.props & Failure) - value -= 15; - if (!sizeLimit || d->fileSize < sizeLimit) - hosts.append({ host, value }); - - // no sorting in preference order. most preferred go first - std::sort(hosts.begin(), hosts.end(), - [](const auto &a, const auto &b) { return a.second > b.second; }); - for (auto &hp : hosts) { - d->httpHosts.append(hp.first); - } + int value = 0; + if (host.props & SecureGet) + value += 5; + if (host.props & SecurePut) + value += 5; + if (host.props & NewestVer) + value += 3; + if (host.props & Failure) + value -= 15; + if (!sizeLimit || d->fileSize < sizeLimit) + hosts.append({ host, value }); + + // no sorting in preference order. most preferred go first + std::sort(hosts.begin(), hosts.end(), [](const auto &a, const auto &b) { return a.second > b.second; }); + for (auto &hp : hosts) { + d->httpHosts.append(hp.first); } } - // d->currentHost = d->httpHosts.begin(); - d->client->httpFileUploadManager()->setDiscoHosts(d->httpHosts); - if (d->httpHosts.isEmpty()) { // if empty as the last resort check all services - d->result.statusCode = HttpFileUpload::ErrorCode::NoUploadService; - d->result.statusString = "No suitable http upload services were found"; - done(State::Error); - } else { - tryNextServer(); - } - }); + } + // d->currentHost = d->httpHosts.begin(); + d->client->httpFileUploadManager()->setDiscoHosts(d->httpHosts); + if (d->httpHosts.isEmpty()) { // if empty as the last resort check all services + d->result.statusCode = HttpFileUpload::ErrorCode::NoUploadService; + d->result.statusString = "No suitable http upload services were found"; + done(State::Error); + } else { + tryNextServer(); + } + }); } void HttpFileUpload::tryNextServer() diff --git a/src/xmpp/xmpp-im/jingle-application.h b/src/xmpp/xmpp-im/jingle-application.h index 3b0edbbc..2fa09472 100644 --- a/src/xmpp/xmpp-im/jingle-application.h +++ b/src/xmpp/xmpp-im/jingle-application.h @@ -21,6 +21,7 @@ #define JINGLE_APPLICATION_H #include "jingle-transport.h" +#include class QTimer; @@ -70,8 +71,8 @@ namespace XMPP { namespace Jingle { Q_DECLARE_FLAGS(ApplicationFlags, ApplicationFlag) virtual void setState(State state) = 0; // likely just remember the state and not generate any signals - virtual XMPP::Stanza::Error lastError() const = 0; - virtual Reason lastReason() const = 0; + virtual const std::optional &lastError() const = 0; + virtual Reason lastReason() const = 0; inline ApplicationManagerPad::Ptr pad() const { return _pad; } inline State state() const { return _state; } diff --git a/src/xmpp/xmpp-im/jingle-connection.cpp b/src/xmpp/xmpp-im/jingle-connection.cpp index a030c890..ea9a4bbf 100644 --- a/src/xmpp/xmpp-im/jingle-connection.cpp +++ b/src/xmpp/xmpp-im/jingle-connection.cpp @@ -23,14 +23,14 @@ namespace XMPP { namespace Jingle { bool Connection::hasPendingDatagrams() const { return false; } - NetworkDatagram Connection::readDatagram(qint64 maxSize) + QNetworkDatagram Connection::readDatagram(qint64 maxSize) { qCritical("Calling unimplemented function receiveDatagram"); Q_UNUSED(maxSize) - return NetworkDatagram(); + return QNetworkDatagram(); } - bool Connection::writeDatagram(const NetworkDatagram &) + bool Connection::writeDatagram(const QNetworkDatagram &) { qCritical("Calling unimplemented function sendDatagram"); return false; @@ -42,10 +42,13 @@ namespace XMPP { namespace Jingle { return 0; } - qint64 Connection::readData(char *, qint64) + qint64 Connection::readData(char *buf, qint64 maxSize) { - qCritical("Calling unimplemented function readData"); - return 0; + auto sz = readDataInternal(buf, maxSize); + if (sz != -1 && _readHook) { + _readHook(buf, sz); + } + return sz; } size_t Connection::blockSize() const diff --git a/src/xmpp/xmpp-im/jingle-connection.h b/src/xmpp/xmpp-im/jingle-connection.h index 486026d1..985d3ffd 100644 --- a/src/xmpp/xmpp-im/jingle-connection.h +++ b/src/xmpp/xmpp-im/jingle-connection.h @@ -28,41 +28,19 @@ #include "iris/bytestream.h" #include "jingle.h" -#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) #include -#else -#include -#endif namespace XMPP { namespace Jingle { -#if QT_VERSION < QT_VERSION_CHECK(5, 8, 0) - // stub implementation - class NetworkDatagram { - public: - bool _valid = false; - QByteArray _data; - inline NetworkDatagram(const QByteArray &data, const QHostAddress &destinationAddress = QHostAddress(), - quint16 port = 0) : _valid(true), _data(data) - { - Q_UNUSED(destinationAddress); - Q_UNUSED(port) - } - inline NetworkDatagram() { } - - inline bool isValid() const { return _valid; } - inline QByteArray data() const { return _data; } - }; -#else - typedef QNetworkDatagram NetworkDatagram; -#endif class Connection : public ByteStream { Q_OBJECT public: - using Ptr = QSharedPointer; // will be shared between transport and application + using Ptr = QSharedPointer; // will be shared between transport and application + using ReadHook = std::function; + virtual bool hasPendingDatagrams() const; - virtual NetworkDatagram readDatagram(qint64 maxSize = -1); - virtual bool writeDatagram(const NetworkDatagram &data); + virtual QNetworkDatagram readDatagram(qint64 maxSize = -1); + virtual bool writeDatagram(const QNetworkDatagram &data); virtual size_t blockSize() const; virtual int component() const; virtual TransportFeatures features() const = 0; @@ -70,6 +48,7 @@ namespace XMPP { namespace Jingle { inline void setId(const QString &id) { _id = id; } inline bool isRemote() const { return _isRemote; } inline void setRemote(bool value) { _isRemote = value; } + inline void setReadHook(ReadHook hook) { _readHook = hook; } signals: void connected(); @@ -77,10 +56,14 @@ namespace XMPP { namespace Jingle { protected: qint64 writeData(const char *data, qint64 maxSize); - qint64 readData(char *data, qint64 maxSize); + qint64 readData(char *buf, qint64 maxSize) final; + + // same rules as for QIOdevice::readData. It was just necessary to wrap it. + virtual qint64 readDataInternal(char *data, qint64 maxSize) = 0; - bool _isRemote = false; - QString _id; + bool _isRemote = false; + QString _id; + ReadHook _readHook; }; using ConnectionAcceptorCallback = std::function; diff --git a/src/xmpp/xmpp-im/jingle-file.cpp b/src/xmpp/xmpp-im/jingle-file.cpp index 29724e3f..3cff1607 100644 --- a/src/xmpp/xmpp-im/jingle-file.cpp +++ b/src/xmpp/xmpp-im/jingle-file.cpp @@ -65,17 +65,16 @@ QDomElement Range::toXml(QDomDocument *doc) const //---------------------------------------------------------------------------- class File::Private : public QSharedData { public: - bool rangeSupported = false; - bool hasSize = false; - QDateTime date; - QString mediaType; - QString name; - QString desc; - std::uint64_t size = 0; - Range range; - QList hashes; - Thumbnail thumbnail; - QByteArray amplitudes; + bool rangeSupported = false; + QDateTime date; + QString mediaType; + QString name; + QString desc; + std::optional size = 0; + Range range; + QList hashes; + Thumbnail thumbnail; + QByteArray amplitudes; }; File::File() { } @@ -92,17 +91,16 @@ File::File(const File &other) : d(other.d) { } File::File(const QDomElement &file) { - QDateTime date; - QString mediaType; - QString name; - QString desc; - std::uint64_t size = 0; - bool rangeSupported = false; - bool hasSize = false; - Range range; - QList hashes; - Thumbnail thumbnail; - QByteArray amplitudes; + QDateTime date; + QString mediaType; + QString name; + QString desc; + std::optional size = 0; + bool rangeSupported = false; + Range range; + QList hashes; + Thumbnail thumbnail; + QByteArray amplitudes; bool ok; @@ -125,7 +123,6 @@ File::File(const QDomElement &file) if (!ok) { return; } - hasSize = true; } else if (ce.tagName() == RANGE_TAG) { if (ce.hasAttribute(QLatin1String("offset"))) { @@ -188,7 +185,6 @@ File::File(const QDomElement &file) p->desc = desc; p->size = size; p->rangeSupported = rangeSupported; - p->hasSize = hasSize; p->range = range; p->hashes = hashes; p->thumbnail = thumbnail; @@ -218,8 +214,8 @@ QDomElement File::toXml(QDomDocument *doc) const if (d->name.size()) { el.appendChild(XMLHelper::textTag(*doc, NAME_TAG, d->name)); } - if (d->hasSize) { - el.appendChild(XMLHelper::textTag(*doc, SIZE_TAG, qint64(d->size))); + if (d->size) { + el.appendChild(XMLHelper::textTag(*doc, SIZE_TAG, qint64(*d->size))); } if (d->rangeSupported || d->range.isValid()) { el.appendChild(d->range.toXml(doc)); @@ -261,8 +257,6 @@ bool File::hasComputedHashes() const return false; } -bool File::hasSize() const { return d->hasSize; } - QDateTime File::date() const { return d ? d->date : QDateTime(); } QString File::description() const { return d ? d->desc : QString(); } @@ -298,7 +292,7 @@ QString File::mediaType() const { return d ? d->mediaType : QString(); } QString File::name() const { return d ? d->name : QString(); } -std::uint64_t File::size() const { return d ? d->size : 0; } +std::optional File::size() const { return d ? d->size : std::optional {}; } Range File::range() const { return d ? d->range : Range(); } @@ -318,11 +312,7 @@ void File::setMediaType(const QString &mediaType) { ensureD()->mediaType = media void File::setName(const QString &name) { ensureD()->name = name; } -void File::setSize(std::uint64_t size) -{ - ensureD()->size = size; - d->hasSize = true; -} +void File::setSize(std::uint64_t size) { ensureD()->size = size; } void File::setRange(const Range &range) { diff --git a/src/xmpp/xmpp-im/jingle-file.h b/src/xmpp/xmpp-im/jingle-file.h index 9bcb0dfd..e5187e4f 100644 --- a/src/xmpp/xmpp-im/jingle-file.h +++ b/src/xmpp/xmpp-im/jingle-file.h @@ -26,15 +26,17 @@ #include #include +#include + namespace XMPP::Jingle::FileTransfer { struct Range { - std::int64_t offset = 0; - std::int64_t length = 0; // 0 - from offset to the end of the file - QList hashes; + std::uint64_t offset = 0; // 0 - default value from spec even when not set. + std::uint64_t length = 0; // 0 - from offset to the end of the file + QList hashes; inline Range() { } - inline Range(std::int64_t offset, std::int64_t length) : offset(offset), length(length) { } - inline bool isValid() const { return hashes.size() || offset || length; } + inline Range(std::uint64_t offset, std::uint64_t length) : offset(offset), length(length) { } + inline bool isValid() const { return offset || length; } inline operator bool() const { return isValid(); } QDomElement toXml(QDomDocument *doc) const; }; @@ -50,19 +52,18 @@ class File { QDomElement toXml(QDomDocument *doc) const; bool merge(const File &other); bool hasComputedHashes() const; - bool hasSize() const; - QDateTime date() const; - QString description() const; - QList hashes() const; - QList computedHashes() const; - Hash hash(Hash::Type t = Hash::Unknown) const; - QString mediaType() const; - QString name() const; - uint64_t size() const; - Range range() const; - Thumbnail thumbnail() const; - QByteArray amplitudes() const; + QDateTime date() const; + QString description() const; + QList hashes() const; + QList computedHashes() const; + Hash hash(Hash::Type t = Hash::Unknown) const; + QString mediaType() const; + QString name() const; + std::optional size() const; + Range range() const; + Thumbnail thumbnail() const; + QByteArray amplitudes() const; void setDate(const QDateTime &date); void setDescription(const QString &desc); diff --git a/src/xmpp/xmpp-im/jingle-ft.cpp b/src/xmpp/xmpp-im/jingle-ft.cpp index b63a60b7..25255d6f 100644 --- a/src/xmpp/xmpp-im/jingle-ft.cpp +++ b/src/xmpp/xmpp-im/jingle-ft.cpp @@ -1,6 +1,6 @@ /* * jignle-ft.h - Jingle file transfer - * Copyright (C) 2019 Sergey Ilinykh + * Copyright (C) 2019-2024 Sergey Ilinykh * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,7 +24,6 @@ #include "xmpp_client.h" #include "xmpp_hash.h" #include "xmpp_thumbs.h" -#include "xmpp_xmlcommon.h" #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) #include @@ -37,8 +36,8 @@ #include #include #include + #include -#include #include using namespace std::chrono_literals; @@ -52,6 +51,22 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { static const QString CHECKSUM_TAG = QStringLiteral("checksum"); static const QString RECEIVED_TAG = QStringLiteral("received"); + class Checksum : public ContentBase { + public: + inline Checksum() { } + Checksum(const QDomElement &file); + bool isValid() const; + QDomElement toXml(QDomDocument *doc) const; + + File file; + }; + + class Received : public ContentBase { + public: + using ContentBase::ContentBase; + QDomElement toXml(QDomDocument *doc) const; + }; + //---------------------------------------------------------------------------- // Checksum //---------------------------------------------------------------------------- @@ -134,21 +149,23 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { Reason updateReason; // Action updateToSend = Action::NoAction; - bool closeDeviceOnFinish = true; - bool streamingMode = false; - bool endlessRange = false; // where range in accepted file doesn't have end - bool outgoingReceived = false; - File file; - File acceptFile; // as it comes with "accept" response - XMPP::Stanza::Error lastError; - Reason lastReason; - Connection::Ptr connection; - QIODevice *device = nullptr; - qint64 bytesLeft = 0; - QList outgoingChecksum; - QList incomingChecksum; - QTimer *finalizeTimer = nullptr; - FileHasher *hasher = nullptr; + bool closeDeviceOnFinish = true; + bool streamingMode = false; + // bool endlessRange = false; // where range in accepted file doesn't have end + bool outgoingReceived = false; + bool writeLoggingStarted = false; + bool readLoggingStarted = false; + File file; + File acceptFile; // as it comes with "accept" response + std::optional lastError; + Reason lastReason; + Connection::Ptr connection; + QIODevice *device = nullptr; + std::optional bytesLeft; + QList outgoingChecksum; + QList incomingChecksum; + QTimer *finalizeTimer = nullptr; + FileHasher *hasher = nullptr; void setState(State s) { @@ -176,17 +193,30 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { setState(State::Finished); } - void handleStreamFail() + void onIncomingChecksum(const QList &hashes) { - lastReason = Reason(Reason::Condition::FailedApplication, QString::fromLatin1("stream failed")); + if (!hasher || q->_senders != q->_pad->session()->peerRole()) { + qDebug("jignle-ft: unexpected incoming checksum. was it negotiated? %s", + qUtf8Printable(q->pad()->session()->peer().full())); + return; + } + incomingChecksum = hashes; + tryFinalizeIncoming(); + } + + void handleStreamFail(const QString &errorMsg = {}) + { + lastReason = Reason(Reason::Condition::FailedApplication, + errorMsg.isEmpty() ? QString::fromLatin1("stream failed") : errorMsg); setState(State::Finished); } void expectReceived() { - qDebug("waiting for "); + qDebug("jingle-ft: waiting for for %s", qUtf8Printable(q->pad()->session()->peer().full())); expectFinalize([this]() { - qDebug("Waiting for timed out. But likely succeeded anyway"); + qDebug("jingle-ft: Waiting for timed out. But likely succeeded anyway. %s", + qUtf8Printable(q->pad()->session()->peer().full())); onReceived(); }); } @@ -216,9 +246,15 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { } } + inline std::size_t getBlockSize() + { + auto sz = connection->blockSize(); + return sz ? sz : 8192; + } + void writeNextBlockToTransport() { - if (!(endlessRange || bytesLeft)) { + if (bytesLeft && *bytesLeft == 0) { if (hasher) { auto hash = hasher->result(); if (hash.isValid()) { @@ -230,21 +266,25 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { expectReceived(); return; // everything is written } - auto sz = qint64(connection->blockSize()); - sz = sz ? sz : 8192; - if (!endlessRange && sz > bytesLeft) { - sz = bytesLeft; + quint64 sz = getBlockSize(); + if (bytesLeft && sz > *bytesLeft) { + sz = *bytesLeft; } QByteArray data; if (device->isSequential()) { - if (!device->bytesAvailable()) + sz = qMin(sz, quint64(device->bytesAvailable())); + if (!sz) return; // we will come back on readyRead - data = device->read(qMin(qint64(sz), device->bytesAvailable())); - } else { - data = device->read(sz); } - if (data.isEmpty()) { - if (endlessRange) { + data.resize(sz); + auto readSz = device->read(data.data(), sz); + if (readSz < 0) { + handleStreamFail(QString::fromLatin1("source device failed")); + return; + } + data.resize(readSz); + if (readSz == 0) { + if (!bytesLeft) { lastReason = Reason(Reason::Condition::Success); if (hasher) { auto hash = hasher->result(); @@ -259,11 +299,10 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { handleStreamFail(); } return; - } - // qDebug("JINGLE-FT write %d bytes to connection", data.size()); - if (hasher) { + } else if (hasher) { hasher->addData(data); } + if (connection->features() & TransportFeature::MessageOriented) { if (!connection->writeDatagram(data)) { handleStreamFail(); @@ -276,20 +315,23 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { } } emit q->progress(device->pos()); - bytesLeft -= data.size(); + if (bytesLeft) { + *bytesLeft -= data.size(); + } } void readNextBlockFromTransport() { - qint64 bytesAvail; - while (bytesLeft && ((bytesAvail = connection->bytesAvailable()) || (connection->hasPendingDatagrams()))) { + quint64 bytesAvail; + while ((!bytesLeft || *bytesLeft > 0) + && ((bytesAvail = connection->bytesAvailable()) || (connection->hasPendingDatagrams()))) { QByteArray data; if (connection->features() & TransportFeature::MessageOriented) { data = connection->readDatagram().data(); } else { - qint64 sz = 65536; // shall we respect transport->blockSize() ? - if (sz > bytesLeft) { - sz = bytesLeft; + quint64 sz = 65536; // shall we respect transport->blockSize() ? + if (bytesLeft && sz > *bytesLeft) { + sz = *bytesLeft; } if (sz > bytesAvail) { sz = bytesAvail; @@ -309,9 +351,11 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { return; } emit q->progress(device->pos()); - bytesLeft -= data.size(); + if (bytesLeft) { + *bytesLeft -= data.size(); + } } - if (!bytesLeft) { + if (bytesLeft && *bytesLeft == 0) { tryFinalizeIncoming(); } } @@ -321,18 +365,48 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { void onConnectionConnected(Connection::Ptr newConnection) { - qDebug("jingle-ft: connected. ready to send user data"); + qDebug("jingle-ft: connected. ready to transfer user data with %s", + qUtf8Printable(q->pad()->session()->peer().full())); connection = newConnection; - lastReason = Reason(); - lastError.reset(); + + lastReason = {}; + lastError = {}; + + if (acceptFile.range().isValid()) { + if (acceptFile.range().length) { + bytesLeft = acceptFile.range().length; + } + } else { + bytesLeft = acceptFile.size(); + } if (streamingMode) { + qDebug("jingle-ft: streaming mode is active for %s", + qUtf8Printable(q->pad()->session()->peer().full())); + if (amIReceiver()) { + connection->setReadHook([this](char *buf, qint64 size) { + // in streaming mode we need this to compute hash sum and detect stream end is size was defined + if (hasher) { + hasher->addData(QByteArray::fromRawData(buf, size)); + } + if (bytesLeft) { + *bytesLeft -= quint64(size); + } + if (bytesLeft && *bytesLeft == 0) { + tryFinalizeIncoming(); + } + }); + } setState(State::Active); emit q->connectionReady(); return; } connect(connection.data(), &Connection::readyRead, q, [this]() { + if (!readLoggingStarted) { + qDebug("jingle-ft: got first readRead for %s", qUtf8Printable(q->pad()->session()->peer().full())); + readLoggingStarted = true; + } if (!device) { return; } @@ -343,8 +417,13 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { connect( connection.data(), &Connection::bytesWritten, q, [this](qint64 bytes) { - Q_UNUSED(bytes) - if (q->pad()->session()->role() == q->senders() && !connection->bytesToWrite()) { + if (!writeLoggingStarted) { + qDebug("jingle-ft: wrote first %lld bytes for %s.", bytes, + qUtf8Printable(q->pad()->session()->peer().full())); + writeLoggingStarted = true; + } + auto bs = getBlockSize(); + if (q->pad()->session()->role() == q->senders() && quint64(connection->bytesToWrite()) < bs) { writeNextBlockToTransport(); } }, @@ -356,28 +435,24 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { setState(State::Active); if (acceptFile.range().isValid()) { - bytesLeft = acceptFile.range().length; - if (!bytesLeft) - endlessRange = true; emit q->deviceRequested(acceptFile.range().offset, bytesLeft); } else { - bytesLeft = acceptFile.size(); emit q->deviceRequested(0, bytesLeft); } } void tryFinalizeIncoming() { - if (q->_state == State::Finished || outgoingReceived || streamingMode) - return; - if (connection->isOpen() && bytesLeft) + auto moreBytesExpected = bytesLeft && *bytesLeft > 0; + if (q->_state == State::Finished || outgoingReceived || (connection->isOpen() && moreBytesExpected)) return; // data read finished. check other stuff if (hasher && incomingChecksum.isEmpty()) { - qDebug("waiting for "); + qDebug("jignle-ft: waiting for with %s", qUtf8Printable(q->pad()->session()->peer().full())); expectFinalize([this]() { - qDebug("Waiting for timed out. But likely succeeded anyway"); + qDebug("jingle-ft: Waiting for timed out. But likely succeeded anyway. %s", + qUtf8Printable(q->pad()->session()->peer().full())); lastReason = Reason(Reason::Condition::Success); setState(State::Finished); }); @@ -402,12 +477,24 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { break; } if (!found) - qDebug("haven't found %s checksum within received checksums", - qPrintable(expectedHash.stringType())); + qDebug("jignle-ft: haven't found %s checksum within received checksums with %s", + qPrintable(expectedHash.stringType()), qUtf8Printable(q->pad()->session()->peer().full())); } outgoingReceived = true; emit q->updated(); } + + void prepareThumbnail(File &file) + { + if (file.thumbnail().data.size()) { + auto client = q->pad()->session()->manager()->client(); + auto thumb = file.thumbnail(); + auto bm = client->bobManager(); + BoBData data = bm->append(thumb.data, thumb.mimeType); + thumb.uri = QLatin1String("cid:") + data.cid(); + file.setThumbnail(thumb); + } + } }; Application::Application(const QSharedPointer &pad, const QString &contentName, Origin creator, @@ -425,12 +512,12 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { Application::~Application() { delete d->hasher; - qDebug("jingle-ft: destroyed"); + qDebug("jingle-ft: destroyed for %s", qUtf8Printable(pad()->session()->peer().full())); } void Application::setState(State state) { d->setState(state); } - Stanza::Error Application::lastError() const { return d->lastError; } + const std::optional &Application::lastError() const { return d->lastError; } Reason Application::lastReason() const { return d->lastReason; } @@ -468,18 +555,6 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { return ret; } - void Application::prepareThumbnail(File &file) - { - if (file.thumbnail().data.size()) { - auto client = _pad->session()->manager()->client(); - auto thumb = file.thumbnail(); - auto bm = client->bobManager(); - BoBData data = bm->append(thumb.data, thumb.mimeType); - thumb.uri = QLatin1String("cid:") + data.cid(); - d->file.setThumbnail(thumb); - } - } - QDomElement Application::makeLocalOffer() { if (!d->file.isValid()) { @@ -488,7 +563,7 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { auto doc = _pad->doc(); auto el = doc->createElementNS(NS, "description"); - prepareThumbnail(d->file); + d->prepareThumbnail(d->file); el.appendChild(d->file.toXml(doc)); return el; } @@ -537,6 +612,8 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { File Application::acceptFile() const { return d->acceptFile; } + void Application::setAcceptFile(const File &file) const { d->acceptFile = file; } + bool Application::isTransportReplaceEnabled() const { return _state < State::Active; } void Application::prepareTransport() @@ -575,7 +652,7 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { OutgoingUpdate Application::takeOutgoingUpdate() { - qDebug("jingle-ft: take outgoing update"); + qDebug("jingle-ft: take outgoing update for %s", qUtf8Printable(pad()->session()->peer().full())); if (_update.action == Action::NoAction) { return OutgoingUpdate(); } @@ -587,8 +664,10 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { if (d->outgoingReceived) { d->outgoingReceived = false; Received received(creator(), _contentName); - return OutgoingUpdate { QList() << received.toXml(doc), - [this](bool) { d->setState(State::Finished); } }; + return OutgoingUpdate { QList() << received.toXml(doc), [this](bool) { + d->lastReason = Reason(Reason::Condition::Success); + d->setState(State::Finished); + } }; } if (!d->outgoingChecksum.isEmpty()) { ContentBase cb(_pad->session()->role(), _contentName); @@ -646,6 +725,7 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { if (_creator == _pad->session()->role() && _state <= State::ApprovedToSend) { // local content, not yet sent to remote + d->lastReason = _terminationReason; setState(State::Finished); return; } @@ -678,23 +758,6 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { Connection::Ptr Application::connection() const { return d->connection.staticCast(); } - void Application::incomingChecksum(const QList &hashes) - { - qDebug("got checksum: %s", qPrintable(hashes.value(0).toString())); - if (!d->hasher || _senders != _pad->session()->peerRole()) { - qDebug("unexpected incoming checksum. was it negotiated?"); - return; - } - d->incomingChecksum = hashes; - d->tryFinalizeIncoming(); - } - - void Application::incomingReceived() - { - qDebug("got received"); - d->onReceived(); - } - Pad::Pad(Manager *manager, Session *session) : _manager(manager), _session(session) { } QDomElement Pad::takeOutgoingSessionInfoUpdate() @@ -729,19 +792,22 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { Checksum checksum(el); auto app = session()->content(checksum.name, checksum.creator); if (app) { - static_cast(app)->incomingChecksum(checksum.file.hashes()); + qDebug("jignle-ft: got checksum: %s for %s", qPrintable(checksum.file.hashes().value(0).toString()), + qUtf8Printable(session()->peer().full())); + static_cast(app)->d->onIncomingChecksum(checksum.file.hashes()); } return true; } else if (el.tagName() == RECEIVED_TAG) { Received received(el); auto app = session()->content(received.name, received.creator); if (app) { - static_cast(app)->incomingReceived(); + qDebug("jingle-ft: got received for %s", qUtf8Printable(session()->peer().full())); + static_cast(app)->d->onReceived(); } return true; } else { // TODO report actual error - qDebug("unknown session-info: %s", qPrintable(el.tagName())); + qDebug("jingle-ft: unknown session-info: %s", qPrintable(el.tagName())); } } return false; diff --git a/src/xmpp/xmpp-im/jingle-ft.h b/src/xmpp/xmpp-im/jingle-ft.h index 589622cb..3b9dc6b7 100644 --- a/src/xmpp/xmpp-im/jingle-ft.h +++ b/src/xmpp/xmpp-im/jingle-ft.h @@ -1,6 +1,6 @@ /* * jignle-ft.h - Jingle file transfer - * Copyright (C) 2019 Sergey Ilinykh + * Copyright (C) 2019-2024 Sergey Ilinykh * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -35,22 +35,6 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { extern const QString NS; class Manager; - class Checksum : public ContentBase { - public: - inline Checksum() { } - Checksum(const QDomElement &file); - bool isValid() const; - QDomElement toXml(QDomDocument *doc) const; - - File file; - }; - - class Received : public ContentBase { - public: - using ContentBase::ContentBase; - QDomElement toXml(QDomDocument *doc) const; - }; - class Pad : public ApplicationManagerPad { Q_OBJECT // TODO @@ -76,62 +60,63 @@ namespace XMPP { namespace Jingle { namespace FileTransfer { Application(const QSharedPointer &pad, const QString &contentName, Origin creator, Origin senders); ~Application() override; - void setState(State state) override; - XMPP::Stanza::Error lastError() const override; - Reason lastReason() const override; + bool isValid() const; + void setState(State state) override; - SetDescError setRemoteOffer(const QDomElement &description) override; - SetDescError setRemoteAnswer(const QDomElement &description) override; - QDomElement makeLocalOffer() override; - QDomElement makeLocalAnswer() override; + const std::optional &lastError() const override; + Reason lastReason() const override; bool isTransportReplaceEnabled() const override; void remove(Reason::Condition cond = Reason::Success, const QString &comment = QString()) override; - XMPP::Jingle::Application::Update evaluateOutgoingUpdate() override; - OutgoingUpdate takeOutgoingUpdate() override; - void prepare() override; - void start() override; - void setFile(const File &file); void setFile(const QFileInfo &fi, const QString &description, const Thumbnail &thumb); File file() const; - File acceptFile() const; + File acceptFile() const; // either local or remote File as an answer to the offer + void setAcceptFile(const File &file) const; /** * @brief setStreamingMode enables external download control. - * So Jingle-FT won't request output device but instead underlying established - * connection will be emitted (see connectionReady() signal). - * The connection is an XMPP::Jingle::Connection::Ptr instance. - * When the connection is not needed anymore, one can just destroy jingle - * session or remove the Application from the session. + * + * When streaming mode is enabled: + * - `connectionReady()` signal to understand when to get ready to use connection + * - `Connection::Ptr connection()` to get connection + * When streaming mode is disabled: + * - `deviceRequested(quint64 offset, optional size)` signal to use `setDevice()` + * - `setDevice(QIODevice *dev, bool closeOnFinish)` to set input/output device + * * Make sure to set the mode before connection is established. * @param mode */ void setStreamingMode(bool mode = true); - bool isValid() const; void setDevice(QIODevice *dev, bool closeOnFinish = true); Connection::Ptr connection() const; - void incomingChecksum(const QList &hashes); - void incomingReceived(); + // next method are used by Jingle::Session and usually shouldn't be called manually + XMPP::Jingle::Application::Update evaluateOutgoingUpdate() override; + OutgoingUpdate takeOutgoingUpdate() override; + void prepare() override; + void start() override; + SetDescError setRemoteOffer(const QDomElement &description) override; + SetDescError setRemoteAnswer(const QDomElement &description) override; protected: + QDomElement makeLocalOffer() override; + QDomElement makeLocalAnswer() override; + void incomingRemove(const Reason &r) override; void prepareTransport() override; - private: - void prepareThumbnail(File &file); - signals: void connectionReady(); // streaming mode only - // if size = 0 then it's reamaining part of the file (non-streaming mode only) - void deviceRequested(qint64 offset, qint64 size); - void progress(qint64 offset); + // if size is not set then it's reamaining part of the file (non-streaming mode only) + void deviceRequested(quint64 offset, std::optional size); + void progress(quint64 offset); private: + friend class Pad; class Private; std::unique_ptr d; }; diff --git a/src/xmpp/xmpp-im/jingle-ibb.cpp b/src/xmpp/xmpp-im/jingle-ibb.cpp index 3b9b5661..e9604a97 100644 --- a/src/xmpp/xmpp-im/jingle-ibb.cpp +++ b/src/xmpp/xmpp-im/jingle-ibb.cpp @@ -108,7 +108,7 @@ namespace XMPP { namespace Jingle { namespace IBB { protected: qint64 writeData(const char *data, qint64 maxSize) { return connection->write(data, maxSize); } - qint64 readData(char *data, qint64 maxSize) + qint64 readDataInternal(char *data, qint64 maxSize) { qint64 ret = connection->read(data, maxSize); if (state == State::Finishing && !bytesAvailable()) { diff --git a/src/xmpp/xmpp-im/jingle-ice.cpp b/src/xmpp/xmpp-im/jingle-ice.cpp index f60a49f4..08f5fea2 100644 --- a/src/xmpp/xmpp-im/jingle-ice.cpp +++ b/src/xmpp/xmpp-im/jingle-ice.cpp @@ -362,9 +362,9 @@ namespace XMPP { namespace Jingle { namespace ICE { enum DisconnectReason { None, DtlsClosed }; - QList datagrams; - DisconnectReason disconnectReason = None; - quint8 componentIndex = 0; + QList datagrams; + DisconnectReason disconnectReason = None; + quint8 componentIndex = 0; RawConnection(quint8 componentIndex) : componentIndex(componentIndex) {}; @@ -378,10 +378,10 @@ namespace XMPP { namespace Jingle { namespace ICE { bool hasPendingDatagrams() const override { return datagrams.size() > 0; } - NetworkDatagram readDatagram(qint64 maxSize = -1) override + QNetworkDatagram readDatagram(qint64 maxSize = -1) override { Q_UNUSED(maxSize) // TODO or not? - return datagrams.size() ? datagrams.takeFirst() : NetworkDatagram(); + return datagrams.size() ? datagrams.takeFirst() : QNetworkDatagram(); } qint64 bytesAvailable() const override { return 0; } @@ -390,6 +390,12 @@ namespace XMPP { namespace Jingle { namespace ICE { void close() override { XMPP::Jingle::Connection::close(); } + qint64 readDataInternal(char *, qint64) override + { + qWarning("jingle-ice: called unsupported RawConnection::readDataInternal"); + return -1; + } + private: friend class Transport; @@ -412,7 +418,7 @@ namespace XMPP { namespace Jingle { namespace ICE { void enqueueIncomingUDP(const QByteArray &data) { - datagrams.append(NetworkDatagram { data }); + datagrams.append(QNetworkDatagram { data }); emit readyRead(); } }; @@ -523,7 +529,7 @@ namespace XMPP { namespace Jingle { namespace ICE { if (componentIndex == 0) { // for other components it's the same but we don't need multiple fingerprints dtls->connect( dtls, &Dtls::needRestart, q, - [this, componentIndex]() { + [this]() { pendingActions |= NewFingerprint; remoteAcceptedFingerprint = false; emit q->updated(); @@ -536,6 +542,7 @@ namespace XMPP { namespace Jingle { namespace ICE { auto d = component.dtls->readDatagram(); #ifdef JINGLE_SCTP if (component.sctp) { + // qDebug("sctp write incoming"); component.sctp->writeIncoming(d); } #endif @@ -544,6 +551,7 @@ namespace XMPP { namespace Jingle { namespace ICE { ice->writeDatagram(componentIndex, components[componentIndex].dtls->readOutgoingDatagram()); }); dtls->connect(dtls, &Dtls::connected, q, [this, componentIndex, dtls]() { + qDebug("Dtls::connected"); auto &c = components[componentIndex]; #ifdef JINGLE_SCTP if (c.sctp) { @@ -726,7 +734,6 @@ namespace XMPP { namespace Jingle { namespace ICE { }, Qt::QueuedConnection); // signal is not DOR-SS q->connect(ice, &Ice176::readyRead, q, [this](int componentIndex) { - // qDebug("ICE readyRead"); auto buf = ice->readDatagram(componentIndex); auto &component = components[componentIndex]; if (component.dtls) { diff --git a/src/xmpp/xmpp-im/jingle-s5b.cpp b/src/xmpp/xmpp-im/jingle-s5b.cpp index 929041bf..35cb6eb5 100644 --- a/src/xmpp/xmpp-im/jingle-s5b.cpp +++ b/src/xmpp/xmpp-im/jingle-s5b.cpp @@ -50,9 +50,9 @@ namespace XMPP { namespace Jingle { namespace S5B { class Connection : public XMPP::Jingle::Connection { Q_OBJECT - QList datagrams; - SocksClient *client = nullptr; - Transport::Mode mode = Transport::Tcp; + QList datagrams; + SocksClient *client = nullptr; + Transport::Mode mode = Transport::Tcp; public: void setSocksClient(SocksClient *client, Transport::Mode mode) @@ -80,10 +80,10 @@ namespace XMPP { namespace Jingle { namespace S5B { bool hasPendingDatagrams() const { return datagrams.size() > 0; } - NetworkDatagram readDatagram(qint64 maxSize = -1) + QNetworkDatagram readDatagram(qint64 maxSize = -1) { Q_UNUSED(maxSize) // TODO or not? - return datagrams.size() ? datagrams.takeFirst() : NetworkDatagram(); + return datagrams.size() ? datagrams.takeFirst() : QNetworkDatagram(); } qint64 bytesAvailable() const @@ -113,22 +113,22 @@ namespace XMPP { namespace Jingle { namespace S5B { { if (mode == Transport::Tcp) return client->write(data, maxSize); - return 0; + return -1; } - qint64 readData(char *data, qint64 maxSize) + qint64 readDataInternal(char *data, qint64 maxSize) { - if (client) + if (client) { return client->read(data, maxSize); - else - return 0; + } else + return -1; } private: friend class Transport; void enqueueIncomingUDP(const QByteArray &data) { - datagrams.append(NetworkDatagram { data }); + datagrams.append(QNetworkDatagram { data }); emit readyRead(); } }; @@ -698,42 +698,42 @@ namespace XMPP { namespace Jingle { namespace S5B { proxyDiscoveryInProgress = true; QList> featureOptions = { { "http://jabber.org/protocol/bytestreams" } }; - q->_pad->session()->manager()->client()->serverInfoManager()->queryServiceInfo( + auto query = q->_pad->session()->manager()->client()->serverInfoManager()->queryServiceInfo( QStringLiteral("proxy"), QStringLiteral("bytestreams"), featureOptions, - QRegularExpression("proxy.*|socks.*|stream.*|s5b.*"), ServerInfoManager::SQ_CheckAllOnNoMatch, - [this](const QList &items) { - if (!proxyDiscoveryInProgress) { // check if new results are ever/still expected - // seems like we have successful connection via higher priority channel. so nobody cares - // about proxy - return; - } - auto m = static_cast(q->_pad->manager()); - Jid userProxy = m->userProxy(); - - bool userProxyFound = !userProxy.isValid(); - for (const auto &i : items) { - quint16 localPref = 0; - if (!userProxyFound && i.jid() == userProxy) { - localPref = 1; - userProxyFound = true; - continue; - } - Candidate c(q, i.jid(), generateCid(), localPref); - localCandidates.emplace(c.cid(), c); - qDebug("new local candidate: %s", qPrintable(c.toString())); - queryS5BProxy(i.jid(), c.cid()); - } - if (!userProxyFound) { - Candidate c(q, userProxy, generateCid(), 1); - localCandidates.emplace(c.cid(), c); - qDebug("new local candidate: %s", qPrintable(c.toString())); - queryS5BProxy(userProxy, c.cid()); - } else if (items.count() == 0) { - // seems like we don't have any proxy - proxyDiscoveryInProgress = false; - checkAndFinishNegotiation(); + QRegularExpression("proxy.*|socks.*|stream.*|s5b.*"), ServiceInfoQuery::CheckAllOnNoMatch); + q->connect(query, &ServiceInfoQuery::finished, q, [this](const QList &items) { + if (!proxyDiscoveryInProgress) { // check if new results are ever/still expected + // seems like we have successful connection via higher priority channel. so nobody cares + // about proxy + return; + } + auto m = static_cast(q->_pad->manager()); + Jid userProxy = m->userProxy(); + + bool userProxyFound = !userProxy.isValid(); + for (const auto &i : items) { + quint16 localPref = 0; + if (!userProxyFound && i.jid() == userProxy) { + localPref = 1; + userProxyFound = true; + continue; } - }); + Candidate c(q, i.jid(), generateCid(), localPref); + localCandidates.emplace(c.cid(), c); + qDebug("new local candidate: %s", qPrintable(c.toString())); + queryS5BProxy(i.jid(), c.cid()); + } + if (!userProxyFound) { + Candidate c(q, userProxy, generateCid(), 1); + localCandidates.emplace(c.cid(), c); + qDebug("new local candidate: %s", qPrintable(c.toString())); + queryS5BProxy(userProxy, c.cid()); + } else if (items.count() == 0) { + // seems like we don't have any proxy + proxyDiscoveryInProgress = false; + checkAndFinishNegotiation(); + } + }); } void tryConnectToRemoteCandidate() @@ -1234,7 +1234,7 @@ namespace XMPP { namespace Jingle { namespace S5B { ce = ce.nextSiblingElement(candidateTag)) { Candidate c(q, ce); if (!c) { - throw Stanza::Error(Stanza::Error::Cancel, Stanza::Error::BadRequest); + throw Stanza::Error(Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::BadRequest); } if (!p2pAllowed && c.type() != Candidate::Proxy) { qDebug("new remote candidate discarded with forbidden p2p: %s", qPrintable(c)); @@ -1262,14 +1262,14 @@ namespace XMPP { namespace Jingle { namespace S5B { if (it == localCandidates.end()) { if (localCandidatesTrack.contains(cid)) return true; // likely discarded as not needed anymore - throw Stanza::Error(Stanza::Error::Cancel, Stanza::Error::ItemNotFound, + throw Stanza::Error(Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::ItemNotFound, QString("failed to find incoming candidate-used candidate %1").arg(cid)); } auto &cUsed = it->second; if (cUsed.state() == Candidate::Pending) { if (cUsed.type() != Candidate::Proxy && !cUsed.isConnected()) { throw Stanza::Error( - Stanza::Error::Cancel, Stanza::Error::NotAcceptable, + Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::NotAcceptable, QString("incoming candidate-used refers a candidate w/o active socks connection: %1") .arg(QString(cUsed))); } @@ -1313,7 +1313,7 @@ namespace XMPP { namespace Jingle { namespace S5B { if (!el.isNull()) { QString cid = el.attribute(QStringLiteral("cid")); if (cid.isEmpty()) { - throw Stanza::Error(Stanza::Error::Cancel, Stanza::Error::ItemNotFound, + throw Stanza::Error(Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::ItemNotFound, "failed to find incoming activated candidate"); } auto c = remoteUsedCandidate; @@ -1334,7 +1334,7 @@ namespace XMPP { namespace Jingle { namespace S5B { if (!el.isNull()) { auto it = localCandidates.find(el.attribute(QStringLiteral("cid"))); if (it == localCandidates.end()) { - throw Stanza::Error(Stanza::Error::Cancel, Stanza::Error::ItemNotFound, + throw Stanza::Error(Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::ItemNotFound, "failed to find incoming proxy-error candidate"); } auto &c = it->second; @@ -1561,7 +1561,7 @@ namespace XMPP { namespace Jingle { namespace S5B { tel.setAttribute(QStringLiteral("dstaddr"), dstaddr); } if (!candidatesToSend.isEmpty()) { - upd = makeUpdate(tel, false, [this, candidatesToSend, initial](Task *jt) mutable { + upd = makeUpdate(tel, false, [this, candidatesToSend](Task *jt) mutable { if (jt->success()) { for (auto &c : candidatesToSend) { if (c.state() == Candidate::Unacked) { diff --git a/src/xmpp/xmpp-im/jingle-sctp-association_p.cpp b/src/xmpp/xmpp-im/jingle-sctp-association_p.cpp index cba6c7f1..663aa122 100644 --- a/src/xmpp/xmpp-im/jingle-sctp-association_p.cpp +++ b/src/xmpp/xmpp-im/jingle-sctp-association_p.cpp @@ -91,7 +91,12 @@ namespace XMPP { namespace Jingle { namespace SCTP { Q_ARG(quint32, ppid)); } - void AssociationPrivate::OnSctpAssociationBufferedAmount(RTC::SctpAssociation *sctpAssociation, uint32_t len) + /** + * @brief AssociationPrivate::OnSctpAssociationBufferedAmount + * @param sctpAssociation + * @param len - number of bytes currently buffered in usrsctp. not more than MAX_SEND_BUFFER_SIZE + */ + void AssociationPrivate::OnSctpAssociationBufferedAmount(RTC::SctpAssociation *sctpAssociation, size_t len) { // qDebug("jignle-sctp: on buffered data: %d", len); Q_UNUSED(sctpAssociation); @@ -157,9 +162,8 @@ namespace XMPP { namespace Jingle { namespace SCTP { consumer.sctpParameters.maxPacketLifeTime = reliable == PartialTimers ? reliability : 0; consumer.sctpParameters.maxRetransmits = reliable == PartialRexmit ? reliability : 0; bool success; - assoc.SendSctpMessage( - &consumer, ppid, reinterpret_cast(data.data()), data.size(), - new std::function([this, &success](bool cb_success) { success = cb_success; })); + assoc.SendSctpMessage(&consumer, ppid, reinterpret_cast(data.data()), data.size(), + new std::function([&success](bool cb_success) { success = cb_success; })); return success; } diff --git a/src/xmpp/xmpp-im/jingle-sctp-association_p.h b/src/xmpp/xmpp-im/jingle-sctp-association_p.h index 312e715c..96091471 100644 --- a/src/xmpp/xmpp-im/jingle-sctp-association_p.h +++ b/src/xmpp/xmpp-im/jingle-sctp-association_p.h @@ -73,7 +73,7 @@ namespace XMPP { namespace Jingle { namespace SCTP { void OnSctpAssociationSendData(RTC::SctpAssociation *, const uint8_t *data, size_t len) override; void OnSctpAssociationMessageReceived(RTC::SctpAssociation *, uint16_t streamId, uint32_t ppid, const uint8_t *msg, size_t len) override; - void OnSctpAssociationBufferedAmount(RTC::SctpAssociation *sctpAssociation, uint32_t len) override; + void OnSctpAssociationBufferedAmount(RTC::SctpAssociation *sctpAssociation, size_t len) override; void OnSctpStreamClosed(RTC::SctpAssociation *sctpAssociation, uint16_t streamId) override; void handleIncomingDataChannelOpen(const QByteArray &data, quint16 streamId); diff --git a/src/xmpp/xmpp-im/jingle-session.cpp b/src/xmpp/xmpp-im/jingle-session.cpp index 0e76a5a3..f50fa037 100644 --- a/src/xmpp/xmpp-im/jingle-session.cpp +++ b/src/xmpp/xmpp-im/jingle-session.cpp @@ -87,7 +87,7 @@ namespace XMPP { namespace Jingle { State state = State::Created; // state of session on our side. if it's incoming we start from Created anyaway // but Pending state is skipped Origin role = Origin::Initiator; // my role in the session - XMPP::Stanza::Error lastError; + std::optional lastError; Reason terminateReason; QMap> applicationPads; QMap> transportPads; @@ -212,7 +212,7 @@ namespace XMPP { namespace Jingle { if (waitingAck) { return; } - lastError = Stanza::Error(0, 0); + lastError = {}; if (!stepTimer.isActive()) { stepTimer.start(); } @@ -363,7 +363,8 @@ namespace XMPP { namespace Jingle { for (const auto &c : std::as_const(initialIncomingUnacceptedContent)) { auto out = c->evaluateOutgoingUpdate(); if (out.action == Action::ContentReject) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); setSessionFinished(); return true; } @@ -375,7 +376,8 @@ namespace XMPP { namespace Jingle { for (const auto &c : std::as_const(contentList)) { auto out = c->evaluateOutgoingUpdate(); if (out.action == Action::ContentRemove) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); setSessionFinished(); return true; } @@ -546,7 +548,8 @@ namespace XMPP { namespace Jingle { std::tie(err, cond, app) = parseContentAdd(ce); if (err == Private::AddContentError::Unparsed) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); qDeleteAll(addSet); return ParseContentListResult(Unparsed, cond, QList(), QList()); } @@ -561,7 +564,7 @@ namespace XMPP { namespace Jingle { if (it == addSet.end()) { rejectSet.insert(contentName, std::make_pair(ce, cond)); - } + } // else it was invalid alternative continue; } @@ -648,12 +651,12 @@ namespace XMPP { namespace Jingle { a->setState(State::Pending); // reset state to pending for already passed validation before // passing error back } - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, err == Private::AddContentError::Unexpected - ? XMPP::Stanza::Error::UnexpectedRequest - : XMPP::Stanza::Error::BadRequest); + ? XMPP::Stanza::Error::ErrorCond::UnexpectedRequest + : XMPP::Stanza::Error::ErrorCond::BadRequest); if (err == Private::AddContentError::Unexpected) { - ErrorUtil::fill(jingleEl.ownerDocument(), lastError, ErrorUtil::OutOfOrder); + ErrorUtil::fill(jingleEl.ownerDocument(), *lastError, ErrorUtil::OutOfOrder); } return std::tuple>(false, QList()); } @@ -667,7 +670,8 @@ namespace XMPP { namespace Jingle { a->setState(State::Pending); // reset state to pending for already passed validation before // passing error back } - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return std::tuple>(false, QList()); } @@ -719,9 +723,10 @@ namespace XMPP { namespace Jingle { switch (err) { case Private::AddContentError::Unparsed: case Private::AddContentError::Unexpected: - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); if (err == Private::AddContentError::Unexpected) { - ErrorUtil::fill(jingleEl.ownerDocument(), lastError, ErrorUtil::OutOfOrder); + ErrorUtil::fill(jingleEl.ownerDocument(), *lastError, ErrorUtil::OutOfOrder); } return false; case Private::AddContentError::Unsupported: @@ -752,7 +757,8 @@ namespace XMPP { namespace Jingle { ce = ce.nextSiblingElement(contentTag)) { ContentBase cb(ce); if (!cb.isValid()) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } Application *app = contentList.value(ContentKey { cb.name, cb.creator }); @@ -792,7 +798,8 @@ namespace XMPP { namespace Jingle { std::tie(parsed, apps) = parseContentAcceptList(jingleEl); if (!parsed) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } @@ -815,7 +822,8 @@ namespace XMPP { namespace Jingle { std::tie(parsed, apps) = parseContentAcceptList(jingleEl); // marks valid apps as accepted if (!parsed) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } @@ -846,13 +854,15 @@ namespace XMPP { namespace Jingle { std::tie(transportParsed, errReason, transport) = parseIncomingTransport(ce); if (!cb.isValid() || !transportParsed) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } Application *app = contentList.value(ContentKey { cb.name, cb.creator }); if (!app || (app->creator() == role && app->state() <= State::Unacked)) { qDebug("not existing app or inaporpriate app state"); - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::ItemNotFound); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::ItemNotFound); return false; } @@ -927,7 +937,8 @@ namespace XMPP { namespace Jingle { auto transportEl = ce.firstChildElement(QString::fromLatin1("transport")); QString transportNS = transportEl.namespaceURI(); if (!cb.isValid() || transportEl.isNull() || transportNS.isEmpty()) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } @@ -977,12 +988,14 @@ namespace XMPP { namespace Jingle { ContentBase cb(ce); if (!cb.isValid() || !(app = q->content(cb.name, cb.creator)) || app->state() >= State::Finishing || !app->transport()) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } auto tel = ce.firstChildElement(QStringLiteral("transport")); if (tel.isNull() || tel.namespaceURI() != app->transport()->pad()->ns()) { - lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::BadRequest); + lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::BadRequest); return false; } updates.append(qMakePair(app->transport(), tel)); @@ -1052,7 +1065,7 @@ namespace XMPP { namespace Jingle { bool Session::isGroupingAllowed() const { return d->groupingAllowed; } - XMPP::Stanza::Error Session::lastError() const { return d->lastError; } + std::optional Session::lastError() const { return d->lastError; } Application *Session::newContent(const QString &ns, Origin senders) { @@ -1211,8 +1224,7 @@ namespace XMPP { namespace Jingle { d->planStep(); return true; case Private::AddContentError::Ok: - if (!apps.size()) - return false; + Q_ASSERT(!apps.isEmpty()); d->initialIncomingUnacceptedContent = apps; for (auto app : std::as_const(apps)) { app->markInitialApplication(true); @@ -1221,15 +1233,16 @@ namespace XMPP { namespace Jingle { d->planStep(); return true; } - - return false; + Q_ASSERT(false); + return false; // unreachable } bool Session::updateFromXml(Action action, const QDomElement &jingleEl) { if (d->state == State::Finished) { - d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::UnexpectedRequest); - ErrorUtil::fill(jingleEl.ownerDocument(), d->lastError, ErrorUtil::OutOfOrder); + d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::UnexpectedRequest); + ErrorUtil::fill(jingleEl.ownerDocument(), *d->lastError, ErrorUtil::OutOfOrder); return false; } @@ -1268,7 +1281,8 @@ namespace XMPP { namespace Jingle { break; } - d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::FeatureNotImplemented); + d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::FeatureNotImplemented); return false; } }} diff --git a/src/xmpp/xmpp-im/jingle-session.h b/src/xmpp/xmpp-im/jingle-session.h index 8f60683e..df062c05 100644 --- a/src/xmpp/xmpp-im/jingle-session.h +++ b/src/xmpp/xmpp-im/jingle-session.h @@ -54,7 +54,7 @@ namespace XMPP { namespace Jingle { bool isGroupingAllowed() const; - XMPP::Stanza::Error lastError() const; + std::optional lastError() const; // make new local content but do not add it to session yet Application *newContent(const QString &ns, Origin senders = Origin::Both); diff --git a/src/xmpp/xmpp-im/jingle-transport.cpp b/src/xmpp/xmpp-im/jingle-transport.cpp index 434ebea6..938fd7d4 100644 --- a/src/xmpp/xmpp-im/jingle-transport.cpp +++ b/src/xmpp/xmpp-im/jingle-transport.cpp @@ -86,6 +86,7 @@ namespace XMPP { namespace Jingle { bool Transport::notifyIncomingConnection(Connection::Ptr connection) const { + qDebug("Transport::notifyIncomingConnection"); for (auto const &acceptor : _connectionAcceptors) { if ((connection->features() & acceptor.features) == acceptor.features && (acceptor.componentIndex < 0 || acceptor.componentIndex == connection->component()) diff --git a/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.cpp b/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.cpp index 501d641e..242d3782 100644 --- a/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.cpp +++ b/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.cpp @@ -22,6 +22,8 @@ #include +#include + namespace XMPP { namespace Jingle { namespace SCTP { WebRTCDataChannel::WebRTCDataChannel(AssociationPrivate *association, quint8 channelType, quint32 reliability, @@ -108,13 +110,18 @@ namespace XMPP { namespace Jingle { namespace SCTP { bool WebRTCDataChannel::hasPendingDatagrams() const { return datagrams.size() > 0; } - NetworkDatagram WebRTCDataChannel::readDatagram(qint64 maxSize) + QNetworkDatagram WebRTCDataChannel::readDatagram(qint64 maxSize) { Q_UNUSED(maxSize) // TODO or not? - return datagrams.size() ? datagrams.takeFirst() : NetworkDatagram(); + if (datagrams.size()) { + auto dg = datagrams.takeFirst(); + _bytesAvailable -= dg.data().size(); + return dg; + } + return {}; } - bool WebRTCDataChannel::writeDatagram(const NetworkDatagram &data) + bool WebRTCDataChannel::writeDatagram(const QNetworkDatagram &data) { Q_ASSERT(bool(outgoingCallback)); outgoingBufSize += data.data().size(); @@ -122,9 +129,37 @@ namespace XMPP { namespace Jingle { namespace SCTP { return true; } - qint64 WebRTCDataChannel::bytesAvailable() const { return 0; } + qint64 WebRTCDataChannel::bytesAvailable() const + { + return tail.size() + _bytesAvailable + Connection::bytesAvailable(); + } + + qint64 WebRTCDataChannel::bytesToWrite() const { return outgoingBufSize + Connection::bytesToWrite(); } - qint64 WebRTCDataChannel::bytesToWrite() const { return 0; /*client->bytesToWrite();*/ } + qint64 WebRTCDataChannel::readDataInternal(char *buf, qint64 sz) + { + qint64 actualSz = 0; + do { + if (tail.isEmpty() && !datagrams.isEmpty()) { + tail = datagrams.takeFirst().data(); + } + auto dataSz = std::min(sz, qint64(tail.size())); + std::memcpy(buf + actualSz, tail.data(), dataSz); + if (dataSz == qint64(tail.size())) { + tail.clear(); + } else { + tail.remove(0, dataSz); + if (!tail.isEmpty()) { + break; + } + } + actualSz += dataSz; + sz -= dataSz; + } while (sz > 0 && !datagrams.isEmpty()); + _bytesAvailable -= actualSz; + // qDebug("read %lld bytes. more %lld is available", actualSz, _bytesAvailable); + return actualSz; + } void WebRTCDataChannel::close() { XMPP::Jingle::Connection::close(); } @@ -172,7 +207,9 @@ namespace XMPP { namespace Jingle { namespace SCTP { return; } // check other PPIDs. - datagrams.append(NetworkDatagram { data }); + datagrams.append(QNetworkDatagram { data }); + _bytesAvailable += data.size(); + // qDebug("datachannel readyread"); emit readyRead(); } diff --git a/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.h b/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.h index 031d30c7..dda718ec 100644 --- a/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.h +++ b/src/xmpp/xmpp-im/jingle-webrtc-datachannel_p.h @@ -49,11 +49,15 @@ namespace XMPP { namespace Jingle { namespace SCTP { using OutgoingCallback = std::function; - AssociationPrivate *association; - QList datagrams; - DisconnectReason disconnectReason = ChannelClosed; - std::size_t outgoingBufSize = 0; - OutgoingCallback outgoingCallback; + AssociationPrivate *association; + + QList datagrams; + quint64 _bytesAvailable = 0; + QByteArray tail; + + DisconnectReason disconnectReason = ChannelClosed; + std::size_t outgoingBufSize = 0; + OutgoingCallback outgoingCallback; quint8 channelType = 0; quint32 reliability = 0; @@ -73,9 +77,10 @@ namespace XMPP { namespace Jingle { namespace SCTP { void setOutgoingCallback(OutgoingCallback &&callback); bool hasPendingDatagrams() const override; - NetworkDatagram readDatagram(qint64 maxSize = -1) override; - bool writeDatagram(const NetworkDatagram &data) override; + QNetworkDatagram readDatagram(qint64 maxSize = -1) override; + bool writeDatagram(const QNetworkDatagram &data) override; qint64 bytesAvailable() const override; + qint64 readDataInternal(char *buf, qint64 sz) override; qint64 bytesToWrite() const override; void close() override; TransportFeatures features() const override; diff --git a/src/xmpp/xmpp-im/jingle.cpp b/src/xmpp/xmpp-im/jingle.cpp index 20ae9c15..b55ffe2f 100644 --- a/src/xmpp/xmpp-im/jingle.cpp +++ b/src/xmpp/xmpp-im/jingle.cpp @@ -404,7 +404,7 @@ namespace XMPP { namespace Jingle { Jingle jingle(jingleEl); if (!jingle.isValid()) { - respondError(iq, Stanza::Error::Cancel, Stanza::Error::BadRequest); + respondError(iq, Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::BadRequest); return true; } @@ -433,13 +433,13 @@ namespace XMPP { namespace Jingle { if (!client()->jingleManager()->isAllowedParty(from) || (!jingle.initiator().isEmpty() && !client()->jingleManager()->isAllowedParty(jingle.initiator()))) { - respondError(iq, Stanza::Error::Cancel, Stanza::Error::ServiceUnavailable); + respondError(iq, Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::ServiceUnavailable); return true; } Jid redirection(client()->jingleManager()->redirectionJid()); if (redirection.isValid()) { - respondError(iq, Stanza::Error::Modify, Stanza::Error::Redirect, + respondError(iq, Stanza::Error::ErrorType::Modify, Stanza::Error::ErrorCond::Redirect, QStringLiteral("xmpp:") + redirection.full()); return true; } @@ -450,13 +450,13 @@ namespace XMPP { namespace Jingle { respondTieBreak(iq); } else { // second session from this peer with the same sid. - respondError(iq, Stanza::Error::Cancel, Stanza::Error::BadRequest); + respondError(iq, Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::BadRequest); } return true; } session = client()->jingleManager()->incomingSessionInitiate(from, jingle, jingleEl); if (!session) { - respondError(iq, client()->jingleManager()->lastError()); + respondError(iq, *client()->jingleManager()->lastError()); return true; } } else { @@ -467,12 +467,13 @@ namespace XMPP { namespace Jingle { client()->send(resp); } else { auto el = client()->doc()->createElementNS(ERROR_NS, QStringLiteral("unknown-session")); - respondError(iq, Stanza::Error::Cancel, Stanza::Error::ItemNotFound, QString(), el); + respondError(iq, Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::ItemNotFound, + QString(), el); } return true; } if (!session->updateFromXml(jingle.action(), jingleEl)) { - respondError(iq, session->lastError()); + respondError(iq, *session->lastError()); return true; } } @@ -498,7 +499,7 @@ namespace XMPP { namespace Jingle { void respondTieBreak(const QDomElement &iq) { - Stanza::Error error(Stanza::Error::Cancel, Stanza::Error::Conflict); + Stanza::Error error(Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::Conflict); ErrorUtil::fill(*client()->doc(), error, ErrorUtil::TieBreak); respondError(iq, error); } @@ -544,7 +545,7 @@ namespace XMPP { namespace Jingle { // when set/valid any incoming session initiate will be replied with redirection error Jid redirectionJid; - XMPP::Stanza::Error lastError; + std::optional lastError; QHash, Session *> sessions; int maxSessions = -1; // no limit @@ -696,7 +697,8 @@ namespace XMPP { namespace Jingle { Session *Manager::incomingSessionInitiate(const Jid &from, const Jingle &jingle, const QDomElement &jingleEl) { if (d->maxSessions > 0 && d->sessions.size() == d->maxSessions) { - d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::Wait, XMPP::Stanza::Error::ResourceConstraint); + d->lastError = XMPP::Stanza::Error(XMPP::Stanza::Error::ErrorType::Wait, + XMPP::Stanza::Error::ErrorCond::ResourceConstraint); return nullptr; } auto key = qMakePair(from, jingle.sid()); @@ -708,7 +710,9 @@ namespace XMPP { namespace Jingle { // transports // QTimer::singleShot(0,[s, this](){ emit incomingSession(s); }); // QMetaObject::invokeMethod(this, "incomingSession", Qt::QueuedConnection, Q_ARG(Session *, s)); - QTimer::singleShot(0, this, [s, this]() { emit incomingSession(s); }); + if (!s->contentList().empty()) { + QTimer::singleShot(0, this, [s, this]() { emit incomingSession(s); }); + } return s; } d->lastError = s->lastError(); @@ -716,7 +720,7 @@ namespace XMPP { namespace Jingle { return nullptr; } - XMPP::Stanza::Error Manager::lastError() const { return d->lastError; } + const std::optional &Manager::lastError() const { return d->lastError; } Session *Manager::newSession(const Jid &j) { @@ -746,7 +750,8 @@ namespace XMPP { namespace Jingle { const char *ErrorUtil::names[ErrorUtil::Last] = { "out-of-order", "tie-break", "unknown-session", "unsupported-info" }; - Stanza::Error ErrorUtil::make(QDomDocument &doc, int jingleCond, int type, int condition, const QString &text) + Stanza::Error ErrorUtil::make(QDomDocument &doc, int jingleCond, Stanza::Error::ErrorType type, + Stanza::Error::ErrorCond condition, const QString &text) { auto el = doc.createElementNS(ERROR_NS, QString::fromLatin1(names[jingleCond - 1])); return Stanza::Error(type, condition, text, el); @@ -773,12 +778,13 @@ namespace XMPP { namespace Jingle { Stanza::Error ErrorUtil::makeTieBreak(QDomDocument &doc) { - return make(doc, TieBreak, XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::Conflict); + return make(doc, TieBreak, XMPP::Stanza::Error::ErrorType::Cancel, XMPP::Stanza::Error::ErrorCond::Conflict); } Stanza::Error ErrorUtil::makeOutOfOrder(QDomDocument &doc) { - return make(doc, OutOfOrder, XMPP::Stanza::Error::Cancel, XMPP::Stanza::Error::UnexpectedRequest); + return make(doc, OutOfOrder, XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond::UnexpectedRequest); } } // namespace Jingle diff --git a/src/xmpp/xmpp-im/jingle.h b/src/xmpp/xmpp-im/jingle.h index 6ea71c1b..1432f1a8 100644 --- a/src/xmpp/xmpp-im/jingle.h +++ b/src/xmpp/xmpp-im/jingle.h @@ -25,6 +25,7 @@ #include #include #include +#include #include @@ -193,9 +194,11 @@ namespace Jingle { static const char *names[Last]; - static XMPP::Stanza::Error make(QDomDocument &doc, int jingleCond, int type = XMPP::Stanza::Error::Cancel, - int condition = XMPP::Stanza::Error::UndefinedCondition, - const QString &text = QString()); + static XMPP::Stanza::Error make(QDomDocument &doc, int jingleCond, + XMPP::Stanza::Error::ErrorType type = XMPP::Stanza::Error::ErrorType::Cancel, + XMPP::Stanza::Error::ErrorCond condition + = XMPP::Stanza::Error::ErrorCond::UndefinedCondition, + const QString &text = QString()); static XMPP::Stanza::Error makeTieBreak(QDomDocument &doc); static XMPP::Stanza::Error makeOutOfOrder(QDomDocument &doc); @@ -371,10 +374,10 @@ namespace Jingle { bool isAllowedParty(const Jid &jid) const; void setRemoteJidChecker(std::function checker); - Session *session(const Jid &remoteJid, const QString &sid); - Session *newSession(const Jid &j); - QString registerSession(Session *session); - XMPP::Stanza::Error lastError() const; + Session *session(const Jid &remoteJid, const QString &sid); + Session *newSession(const Jid &j); + QString registerSession(Session *session); + const std::optional &lastError() const; void detachSession(Session *s); // disconnect the session from manager signals: diff --git a/src/xmpp/xmpp-im/s5b.cpp b/src/xmpp/xmpp-im/s5b.cpp index 0398ac63..c8918315 100644 --- a/src/xmpp/xmpp-im/s5b.cpp +++ b/src/xmpp/xmpp-im/s5b.cpp @@ -607,7 +607,7 @@ void S5BManager::ps_incoming(const S5BRequest &req) } } if (!ok) { - d->ps->respondError(req.from, req.id, Stanza::Error::NotAcceptable, "SID in use"); + d->ps->respondError(req.from, req.id, Stanza::Error::ErrorCond::NotAcceptable, "SID in use"); return; } @@ -801,7 +801,7 @@ void S5BManager::con_accept(S5BConnection *c) void S5BManager::con_reject(S5BConnection *c) { - d->ps->respondError(c->d->peer, c->d->req.id, Stanza::Error::NotAcceptable, "Not acceptable"); + d->ps->respondError(c->d->peer, c->d->req.id, Stanza::Error::ErrorCond::NotAcceptable, "Not acceptable"); } void S5BManager::con_unlink(S5BConnection *c) @@ -812,7 +812,7 @@ void S5BManager::con_unlink(S5BConnection *c) // active incoming request? cancel it if (e->i && e->i->conn) - d->ps->respondError(e->i->peer, e->i->out_id, Stanza::Error::NotAcceptable, "Not acceptable"); + d->ps->respondError(e->i->peer, e->i->out_id, Stanza::Error::ErrorCond::NotAcceptable, "Not acceptable"); delete e->i; d->activeList.removeAll(e); delete e; @@ -1078,7 +1078,7 @@ void S5BManager::Item::handleFast(const StreamHostList &hosts, const QString &iq // if we already have a stream, then bounce this request if (client) { - m->doError(peer, iq_id, Stanza::Error::NotAcceptable, "Not acceptable"); + m->doError(peer, iq_id, Stanza::Error::ErrorCond::NotAcceptable, "Not acceptable"); } else { in_hosts = hosts; in_id = iq_id; @@ -1442,7 +1442,7 @@ void S5BManager::Item::sc_error(int) void S5BManager::Item::doConnectError() { localFailed = true; - m->doError(peer, in_id, Stanza::Error::RemoteServerNotFound, "Could not connect to given hosts"); + m->doError(peer, in_id, Stanza::Error::ErrorCond::RemoteServerNotFound, "Could not connect to given hosts"); checkFailure(); } @@ -2075,7 +2075,7 @@ void JT_PushS5B::respondSuccess(const Jid &to, const QString &id, const Jid &str void JT_PushS5B::respondError(const Jid &to, const QString &id, Stanza::Error::ErrorCond cond, const QString &str) { QDomElement iq = createIQ(doc(), "error", to.full(), id); - Stanza::Error error(Stanza::Error::Cancel, cond, str); + Stanza::Error error(Stanza::Error::ErrorType::Cancel, cond, str); iq.appendChild(error.toXml(*client()->doc(), client()->stream().baseNS())); send(iq); } diff --git a/src/xmpp/xmpp-im/stundisco.cpp b/src/xmpp/xmpp-im/stundisco.cpp index 0ef2405c..21ce1f81 100644 --- a/src/xmpp/xmpp-im/stundisco.cpp +++ b/src/xmpp/xmpp-im/stundisco.cpp @@ -87,7 +87,7 @@ class StunDiscoMonitor : public AbstractStunDisco { needResolve.append(s); else s->addresses.append(addr); - if (l->restricted && s->password.isEmpty()) { + if (l->needsNewCreds()) { needCreds.append(s); } else if (!s->addresses.isEmpty()) { emit serviceAdded(s); @@ -179,13 +179,14 @@ class StunDiscoMonitor : public AbstractStunDisco { QString etype = extType(s); auto it = std::find_if(resolved.begin(), resolved.end(), [&s, &etype](ExternalService::Ptr const &r) { - return s->host == r->host && etype == r->type && s->port == r->port; + return s->host == r->host && etype == r->type && (r->port == 0 || s->port == r->port); }); if (it == resolved.end()) { s->expires = QDeadlineTimer(); // expired timer qDebug("no creds from server for %s:%hu %s", qPrintable(s->host), s->port, qPrintable(etype)); continue; // failed to get creds? weird } + s->expires = (*it)->expires; s->username = (*it)->username; s->password = (*it)->password; } diff --git a/src/xmpp/xmpp-im/types.cpp b/src/xmpp/xmpp-im/types.cpp index 68137fa9..14e92c50 100644 --- a/src/xmpp/xmpp-im/types.cpp +++ b/src/xmpp/xmpp-im/types.cpp @@ -31,6 +31,8 @@ #include #include +#include + #define NS_XML "http://www.w3.org/XML/1998/namespace" namespace XMPP { @@ -666,8 +668,9 @@ void HTMLElement::filterOutUnwantedRecursive(QDomElement &el, bool strict) { Q_UNUSED(strict) // TODO filter out not xhtml-im elements - static QSet unwanted = QSet() << "script" << "iframe"; - QDomNode child = el.firstChild(); + static QSet unwanted = QSet() << "script" + << "iframe"; + QDomNode child = el.firstChild(); while (!child.isNull()) { QDomNode sibling = child.nextSibling(); if (child.isElement()) { @@ -699,13 +702,14 @@ void HTMLElement::filterOutUnwantedRecursive(QDomElement &el, bool strict) //---------------------------------------------------------------------------- class Message::Private : public QSharedData { public: - Jid to, from; - QString id, type, lang; + Jid to, from; + QString id, lang; + Message::Type type = Message::Type::Normal; - StringMap subject, body; - QString thread; bool threadSend = false; bool pureSubject = false; // set during parsing is subject exists body doesn't + StringMap subject, body; + QString thread; Stanza::Error error; // extensions @@ -748,6 +752,8 @@ class Message::Private : public QSharedData { Message::StanzaId stanzaId; // XEP-0359 QList references; // XEP-0385 and XEP-0372 Forwarding forwarding; // XEP-0297 + Message::Reactions reactions; // XEP-0444 + QString retraction; // XEP-0424 }; #define MessageD() (d ? d : (d = new Private)) @@ -790,7 +796,27 @@ Jid Message::from() const { return d ? d->from : Jid(); } QString Message::id() const { return d ? d->id : QString(); } //! \brief Return type information -QString Message::type() const { return d ? d->type : QString(); } +Message::Type Message::type() const { return d ? d->type : Type::Normal; } + +QString Message::typeStr() const +{ + if (d) { + switch (d->type) { + case Type::Chat: + return QStringLiteral("chat"); + case Type::Error: + return QStringLiteral("error"); + case Type::Groupchat: + return QStringLiteral("groupchat"); + case Type::Headline: + return QStringLiteral("headline"); + case Type::Normal: + default: + break; + } + } + return {}; +} QString Message::lang() const { return d ? d->lang : QString(); } @@ -880,9 +906,9 @@ void Message::setId(const QString &s) //! \brief Set Type of message //! //! \param type - type of message your going to send -void Message::setType(const QString &s) +void Message::setType(Type type) { - MessageD()->type = s; + MessageD()->type = type; // d->flag = false; } @@ -1055,7 +1081,7 @@ QString Message::mucPassword() const { return d ? d->mucPassword : QString(); } void Message::setMUCPassword(const QString &p) { MessageD()->mucPassword = p; } -bool Message::hasMUCUser() const { return d & d->hasMUCUser; } +bool Message::hasMUCUser() const { return d && d->hasMUCUser; } Message::StanzaId Message::stanzaId() const { return d ? d->stanzaId : StanzaId(); } @@ -1075,6 +1101,14 @@ void Message::addReference(const Reference &r) { MessageD()->references.append(r void Message::setReferences(const QList &r) { MessageD()->references = r; } +void Message::setReactions(const XMPP::Message::Reactions &reactions) { MessageD()->reactions = reactions; } + +XMPP::Message::Reactions Message::reactions() const { return d ? d->reactions : Reactions {}; } + +void Message::setRetraction(const QString &retractedMessageId) { MessageD()->retraction = retractedMessageId; } + +QString Message::retraction() const { return d ? d->retraction : QString {}; } + QString Message::invite() const { return d ? d->invite : QString(); } void Message::setInvite(const QString &s) { MessageD()->invite = s; } @@ -1161,7 +1195,8 @@ Stanza Message::toStanza(Stream *stream) const if (!d) { return Stanza(); } - Stanza s = stream->createStanza(Stanza::Message, d->to, d->type); + + Stanza s = stream->createStanza(Stanza::Message, d->to, typeStr()); if (!d->from.isEmpty()) s.setFrom(d->from); if (!d->id.isEmpty()) @@ -1197,7 +1232,7 @@ Stanza Message::toStanza(Stream *stream) const } } - if (d->type == "error") + if (d->type == Type::Error) s.setError(d->error); // thread @@ -1442,6 +1477,26 @@ Stanza Message::toStanza(Stream *stream) const s.appendChild(r.toXml(&s.doc())); } + // XEP-0444 + auto reactionsNS = QStringLiteral("urn:xmpp:reactions:0"); + if (!d->reactions.targetId.isEmpty()) { + auto e = s.createElement(reactionsNS, QStringLiteral("reactions")); + e.setAttribute(QLatin1String("id"), d->reactions.targetId); + for (const QString &reaction : d->reactions.reactions) { + e.appendChild(s.createTextElement(reactionsNS, QStringLiteral("reaction"), reaction)); + } + s.appendChild(e); + s.appendChild(s.createElement(QStringLiteral("urn:xmpp:hints"), QStringLiteral("store"))); + } + + // XEP-0424 + if (!d->retraction.isEmpty()) { + auto e = s.createElement("urn:xmpp:message-retract:1", QStringLiteral("retract")); + e.setAttribute(QLatin1String("id"), d->retraction); + s.appendChild(e); + s.appendChild(s.createElement(QStringLiteral("urn:xmpp:hints"), QStringLiteral("store"))); + } + return s; } @@ -1472,9 +1527,21 @@ bool Message::fromStanza(const Stanza &s, bool useTimeZoneOffset, int timeZoneOf setTo(s.to()); setFrom(s.from()); setId(s.id()); - setType(s.type()); setLang(s.lang()); + auto typeStr = s.type(); + if (typeStr == "chat") { + setType(Type::Chat); + } else if (typeStr == "error") { + setType(Type::Error); + } else if (typeStr == "groupchat") { + setType(Type::Groupchat); + } else if (typeStr == "headline") { + setType(Type::Headline); + } else { + setType(Type::Normal); // everything unknown is normal by rfc6121 + } + d->subject.clear(); d->body.clear(); d->htmlElements.clear(); @@ -1782,13 +1849,34 @@ bool Message::fromStanza(const Stanza &s, bool useTimeZoneOffset, int timeZoneOf } } + // XEP-0444 message reactions + auto reactions + = childElementsByTagNameNS(root, "urn:xmpp:reactions:0", QStringLiteral("reactions")).item(0).toElement(); + if (!reactions.isNull()) { + d->reactions.targetId = reactions.attribute(QLatin1String("id")); + if (!d->reactions.targetId.isEmpty()) { + auto reactionTag = QStringLiteral("reaction"); + auto reaction = reactions.firstChildElement(reactionTag); + while (!reaction.isNull()) { + d->reactions.reactions.insert(reaction.text().trimmed()); + reaction = reaction.nextSiblingElement(reactionTag); + } + d->reactions.reactions.squeeze(); + } + } + + // XEP-0424 message retraction + d->retraction = childElementsByTagNameNS(root, "urn:xmpp:message-retract:1", QStringLiteral("retract")) + .item(0) + .toElement() + .attribute(QLatin1String("id")); return true; } /*! Error object used to deny a request. */ -Stanza::Error HttpAuthRequest::denyError(Stanza::Error::Auth, Stanza::Error::NotAuthorized); +Stanza::Error HttpAuthRequest::denyError(Stanza::Error::ErrorType::Auth, Stanza::Error::ErrorCond::NotAuthorized); /*! Constructs request of resource URL \a u, made by method \a m, with transaction id \a i. @@ -2064,13 +2152,13 @@ class StatusPrivate : public QSharedData { public: StatusPrivate() = default; - int priority = 0; - QString show, status, key; - QDateTime timeStamp; - bool isAvailable = false; - bool isInvisible = false; - QByteArray photoHash; - bool hasPhotoHash = false; + int priority = 0; + QString show, status, key; + QDateTime timeStamp; + bool isAvailable = false; + bool isInvisible = false; + + std::optional photoHash; QString xsigned; // gabber song extension @@ -2227,15 +2315,9 @@ void Status::setMUCHistory(int maxchars, int maxstanzas, int seconds, const QDat d->mucHistorySince = since; } -const QByteArray &Status::photoHash() const { return d->photoHash; } - -void Status::setPhotoHash(const QByteArray &h) -{ - d->photoHash = h; - d->hasPhotoHash = true; -} +const std::optional &Status::photoHash() const { return d->photoHash; } -bool Status::hasPhotoHash() const { return d->hasPhotoHash; } +void Status::setPhotoHash(const QByteArray &h) { d->photoHash = h; } void Status::addBoBData(const BoBData &bob) { d->bobDataList.append(bob); } diff --git a/src/xmpp/xmpp-im/xmpp_discoitem.cpp b/src/xmpp/xmpp-im/xmpp_discoitem.cpp index f959365e..a8e7fc3f 100644 --- a/src/xmpp/xmpp-im/xmpp_discoitem.cpp +++ b/src/xmpp/xmpp-im/xmpp_discoitem.cpp @@ -219,6 +219,17 @@ QDomElement DiscoItem::toDiscoInfoResult(QDomDocument *doc) const return q; } +bool DiscoItem::hasPersistentStorage() const +{ + bool hasPEP = false; + for (const DiscoItem::Identity &i : d->identities) { + if (i.category == "pubsub" && i.type == "pep") { + hasPEP = true; + } + } + return hasPEP && d->features.test("http://jabber.org/protocol/pubsub#publish-options"); +} + const Jid &DiscoItem::jid() const { return d->jid; } void DiscoItem::setJid(const Jid &j) { d->jid = j; } diff --git a/src/xmpp/xmpp-im/xmpp_discoitem.h b/src/xmpp/xmpp-im/xmpp_discoitem.h index f1022617..c20010a6 100644 --- a/src/xmpp/xmpp-im/xmpp_discoitem.h +++ b/src/xmpp/xmpp-im/xmpp_discoitem.h @@ -93,6 +93,8 @@ class DiscoItem { static DiscoItem fromDiscoInfoResult(const QDomElement &x); QDomElement toDiscoInfoResult(QDomDocument *doc) const; + bool hasPersistentStorage() const; + private: QSharedDataPointer d; }; diff --git a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp index c598b9e6..159613c5 100644 --- a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp +++ b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.cpp @@ -74,7 +74,7 @@ bool JT_ExternalServiceDiscovery::take(const QDomElement &x) for (auto el = query.firstChildElement(serviceTag); !el.isNull(); el = el.nextSiblingElement(serviceTag)) { // services_.append(ExternalService {}); auto s = std::make_shared(); - if (s->parse(el)) { + if (s->parse(el, !creds_.isEmpty(), false)) { services_.append(s); } } @@ -86,7 +86,43 @@ bool JT_ExternalServiceDiscovery::take(const QDomElement &x) return true; } -bool ExternalService::parse(QDomElement &el) +//---------------------------------------------------------------------------- +// JT_PushMessage +//---------------------------------------------------------------------------- +class JT_PushExternalService : public Task { + + Q_OBJECT + + ExternalServiceList services_; + +public: + using Task::Task; + bool take(const QDomElement &e) + { + if (e.tagName() != QStringLiteral("iq") || e.attribute("type") != QStringLiteral("set")) + return false; + auto query = e.firstChildElement(QLatin1String("services")); + if (query.isNull() || query.namespaceURI() != QLatin1String("urn:xmpp:extdisco:2")) { + return false; + } + QString serviceTag { QStringLiteral("service") }; + for (auto el = query.firstChildElement(serviceTag); !el.isNull(); el = el.nextSiblingElement(serviceTag)) { + // services_.append(ExternalService {}); + auto s = std::make_shared(); + if (s->parse(el, false, true)) { + services_.append(s); + } + } + emit received(services_); + + return true; + } + +signals: + void received(const ExternalServiceList &); +}; + +bool ExternalService::parse(QDomElement &el, bool isCreds, bool isPush) { QString actionOpt = el.attribute(QLatin1String("action")); QString expiresOpt = el.attribute(QLatin1String("expires")); @@ -104,7 +140,7 @@ bool ExternalService::parse(QDomElement &el) return false; port = portReq.toUShort(&ok); - if (!ok) + if (!ok && !portReq.isEmpty()) return false; if (!expiresOpt.isEmpty()) { @@ -116,25 +152,38 @@ bool ExternalService::parse(QDomElement &el) if (expires.hasExpired()) qInfo("Server returned already expired service %s expired at %s UTC", qPrintable(*this), qPrintable(expiresOpt)); - } else { - expires = QDeadlineTimer(QDeadlineTimer::Forever); - } + } // else never expires - if (actionOpt.isEmpty() || actionOpt == QLatin1String("add")) - action = Action::Add; - else if (actionOpt == QLatin1String("modify")) - action = Action::Modify; - else if (actionOpt == QLatin1String("delete")) - action = Action::Delete; - else - return false; + if (isCreds) { + return true; // just host/type/username/password/expires and optional port + } + restricted = !username.isEmpty() || !password.isEmpty(); if (!restrictedOpt.isEmpty()) { if (restrictedOpt == QLatin1String("true") || restrictedOpt == QLatin1String("1")) restricted = true; else if (restrictedOpt != QLatin1String("false") && restrictedOpt != QLatin1String("0")) return false; } + if (restricted && username.isEmpty() && password.isEmpty() && expiresOpt.isEmpty()) { + expires = QDeadlineTimer(); // restricted but creds invalid. make expired + } + + auto formEl = childElementsByTagNameNS(el, "jabber:x:data", "x").item(0).toElement(); + if (!formEl.isNull()) { + form.fromXml(formEl); + } + + if (isPush) { + if (actionOpt.isEmpty() || actionOpt == QLatin1String("add")) + action = Action::Add; + else if (actionOpt == QLatin1String("modify")) + action = Action::Modify; + else if (actionOpt == QLatin1String("delete")) + action = Action::Delete; + else + return false; + } return true; } @@ -145,11 +194,63 @@ ExternalService::operator QString() const .arg(name, host, QString::number(port), type, transport); } -ExternalServiceDiscovery::ExternalServiceDiscovery(Client *client) : client_(client) { } +bool ExternalService::needsNewCreds(std::chrono::minutes minTtl) const +{ + return restricted || !(expires.isForever() || expires.remainingTimeAsDuration() > minTtl); +} + +ExternalServiceDiscovery::ExternalServiceDiscovery(Client *client) : client_(client) +{ + JT_PushExternalService *push = new JT_PushExternalService(client->rootTask()); + connect(push, &JT_PushExternalService::received, this, [this](const ExternalServiceList &services) { + ExternalServiceList deleted; + ExternalServiceList modified; + ExternalServiceList added; + for (auto const &service : services) { + auto cachedServiceIt = findCachedService({ service->host, service->type, service->port }); + if (cachedServiceIt != services_.end()) { + switch (service->action) { + case ExternalService::Add: // weird.. + modified << service; + **cachedServiceIt = *service; + break; + case ExternalService::Modify: + modified << service; + **cachedServiceIt = *service; + break; + case ExternalService::Delete: + deleted << *cachedServiceIt; + services_.erase(cachedServiceIt); + break; + } + } else { + switch (service->action) { + case ExternalService::Add: + case ExternalService::Modify: // weird.. + added << service; + services_ << service; + break; + case ExternalService::Delete: + // we never knew it + break; + } + } + } + if (!added.empty()) { + emit serviceAdded(added); + } + if (!modified.empty()) { + emit serviceModified(modified); + } + if (!deleted.empty()) { + emit serviceDeleted(deleted); + } + }); +} bool ExternalServiceDiscovery::isSupported() const { - return client_->serverInfoManager()->features().test("urn:xmpp:extdisco:2"); + return client_->serverInfoManager()->serverFeatures().test("urn:xmpp:extdisco:2"); } void ExternalServiceDiscovery::services(QObject *ctx, ServicesCallback &&callback, std::chrono::minutes minTtl, @@ -194,12 +295,20 @@ void ExternalServiceDiscovery::services(QObject *ctx, ServicesCallback &&callbac } else { auto task = new JT_ExternalServiceDiscovery(client_->rootTask()); auto type = types[0]; - connect(task, &Task::finished, ctx, - [this, task, type, cb = std::move(callback)]() { cb(task->services()); }); + connect(task, &Task::finished, ctx, [task, type, cb = std::move(callback), this]() { + for (auto const &service : std::as_const(task->services())) { + auto cachedServiceIt = findCachedService({ service->host, service->type, service->port }); + if (cachedServiceIt != services_.end()) { + **cachedServiceIt = *service; + } // else we can't add to the cache coz it can make the cache incomplete. see + // comment below. + } + cb(task->services()); + }); task->getServices(type); task->go(true); - // in fact we can improve caching even more if start remembering specific repviously requested types, - // even if the result was negative. + // in fact we can improve caching even more if start remembering specific pveviously + // requested types, even if the result was negative. } } } @@ -218,13 +327,57 @@ ExternalServiceList ExternalServiceDiscovery::cachedServices(const QStringList & } void ExternalServiceDiscovery::credentials(QObject *ctx, ServicesCallback &&callback, - const QSet &ids) + const QSet &ids, std::chrono::minutes minTtl) { + bool cacheValid = true; + ExternalServiceList ret; + for (auto const &id : ids) { + auto cachedServiceIt = findCachedService(id); + if (cachedServiceIt != services_.end()) { + auto const &s = **cachedServiceIt; + if (s.username.isEmpty() || s.password.isEmpty() + || !(s.expires.isForever() || s.expires.remainingTimeAsDuration() > minTtl)) { + cacheValid = false; + break; + } + ret << *cachedServiceIt; + } + } + if (cacheValid) { + callback(ret); + return; + } + auto task = new JT_ExternalServiceDiscovery(client_->rootTask()); - connect(task, &Task::finished, ctx ? ctx : this, - [this, task, cb = std::move(callback)]() { cb(task->services()); }); + connect(task, &Task::finished, ctx ? ctx : this, [task, cb = std::move(callback), this]() { + ExternalServiceList ret; + for (auto const &service : std::as_const(task->services())) { + auto cachedServiceIt = findCachedService({ service->host, service->type, service->port }); + if (cachedServiceIt != services_.end()) { + auto &cache = **cachedServiceIt; + cache.username = service->username; + cache.password = service->password; + cache.expires = service->expires; + ret << *cachedServiceIt; + } else { + qDebug("credentials request returned creds not previously cached service. adding to " + "the result as is."); + ret << service; + } + } + cb(ret); + }); task->getCredentials(ids); task->go(true); } +ExternalServiceList::iterator ExternalServiceDiscovery::findCachedService(const ExternalServiceId &id) +{ + return std::find_if(services_.begin(), services_.end(), [&id](auto const &s) { + return s->type == id.type && s->host == id.host && (id.port == 0 || s->port == id.port); + }); +} + } // namespace XMPP + +#include "xmpp_externalservicediscovery.moc" diff --git a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h index 563603c2..a31bd8e2 100644 --- a/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h +++ b/src/xmpp/xmpp-im/xmpp_externalservicediscovery.h @@ -21,6 +21,7 @@ #define XMPP_EXTERNALSERVICEDISCOVERY_H #include "xmpp_task.h" +#include "xmpp_xdata.h" #include #include @@ -40,19 +41,22 @@ struct ExternalService { using Ptr = std::shared_ptr; enum Action { Add, Delete, Modify }; - Action action = Action::Add; - QDeadlineTimer expires; // optional - QString host; // required - QString name; // optional - QString password; // optional - std::uint16_t port; // required - bool restricted = false; // optional - QString transport; // optional - QString type; // required - QString username; // optional - - bool parse(QDomElement &el); + Action action = Action::Add; // required only for pushes + QDeadlineTimer expires = QDeadlineTimer::Forever; // optional + QString host; // required + QString name; // optional + QString password; // optional + std::uint16_t port; // required + bool restricted = false; // optional + QString transport; // optional + QString type; // required + QString username; // optional + XData form; // optional + + bool parse(QDomElement &el, bool isCreds, bool isPush); operator QString() const; + + bool needsNewCreds(std::chrono::minutes minTtl = std::chrono::minutes(1)) const; }; struct ExternalServiceId { @@ -122,6 +126,7 @@ class ExternalServiceDiscovery : public QObject { * @param ctx - if ctx dies, the request will be aborted * @param callback - callback to call when ready * @param ids - identifier of services + * @param minTtl - if service expires in less than minTtl it will be re-requested * * The credentials won't be cached since it's assumed if crdentials are returned with services request then * the creds are constant values until the service is expired. @@ -129,7 +134,8 @@ class ExternalServiceDiscovery : public QObject { * Most likely with `restricted` flag those are going to be temporal credentials. Even so the caller may cache them * on its own risk. */ - void credentials(QObject *ctx, ServicesCallback &&callback, const QSet &ids); + void credentials(QObject *ctx, ServicesCallback &&callback, const QSet &ids, + std::chrono::minutes minTtl = std::chrono::minutes(1)); signals: // server push signals only void serviceAdded(const ExternalServiceList &); @@ -137,6 +143,8 @@ class ExternalServiceDiscovery : public QObject { void serviceModified(ExternalServiceList &); private: + ExternalServiceList::iterator findCachedService(const ExternalServiceId &id = {}); + Client *client_; QPointer currentTask = nullptr; // for all services (no type) ExternalServiceList services_; diff --git a/src/xmpp/xmpp-im/xmpp_features.cpp b/src/xmpp/xmpp-im/xmpp_features.cpp index 8aa1f5be..2945409b 100644 --- a/src/xmpp/xmpp-im/xmpp_features.cpp +++ b/src/xmpp/xmpp-im/xmpp_features.cpp @@ -182,6 +182,15 @@ bool Features::hasVCard() const return test(ns); } +#define FID_VCARD4 "urn:ietf:params:xml:ns:vcard-4.0" +bool Features::hasVCard4() const +{ + QSet ns; + ns << FID_VCARD4; + + return test(ns); +} + #define FID_MESSAGECARBONS "urn:xmpp:carbons:2" bool Features::hasMessageCarbons() const { @@ -214,6 +223,12 @@ bool Features::hasCapsOptimize() const { return test(QStringList() << QLatin1Str #define NS_DIRECT_MUC_INVITE "jabber:x:conference" bool Features::hasDirectMucInvite() const { return test(QStringList() << QLatin1String(NS_DIRECT_MUC_INVITE)); } +#define FID_AVATAR_TO_VCARD_CONVERSION "urn:xmpp:pep-vcard-conversion:0" +bool Features::hasAvatarConversion() const +{ + return test(QStringList() << QLatin1String(FID_AVATAR_TO_VCARD_CONVERSION)); +} + // custom Psi actions #define FID_ADD "psi:add" diff --git a/src/xmpp/xmpp-im/xmpp_features.h b/src/xmpp/xmpp-im/xmpp_features.h index 6b7f3885..0ee18f23 100644 --- a/src/xmpp/xmpp-im/xmpp_features.h +++ b/src/xmpp/xmpp-im/xmpp_features.h @@ -41,24 +41,27 @@ class Features { // features inline bool isEmpty() const { return _list.isEmpty(); } - bool hasRegister() const; - bool hasSearch() const; - bool hasMulticast() const; - bool hasGroupchat() const; - bool hasVoice() const; - bool hasDisco() const; - bool hasChatState() const; - bool hasCommand() const; - bool hasGateway() const; - bool hasVersion() const; - bool hasVCard() const; - bool hasMessageCarbons() const; - bool hasJingleFT() const; - bool hasJingleIceUdp() const; - bool hasJingleIce() const; - bool hasCaps() const; - bool hasCapsOptimize() const; - bool hasDirectMucInvite() const; + + bool hasRegister() const; + bool hasSearch() const; + bool hasMulticast() const; + bool hasGroupchat() const; + bool hasVoice() const; + bool hasDisco() const; + bool hasChatState() const; + bool hasCommand() const; + bool hasGateway() const; + bool hasVersion() const; + bool hasVCard() const; + bool hasVCard4() const; + bool hasMessageCarbons() const; + bool hasJingleFT() const; + bool hasJingleIceUdp() const; + bool hasJingleIce() const; + bool hasCaps() const; + bool hasCapsOptimize() const; + bool hasDirectMucInvite() const; + bool hasAvatarConversion() const; [[deprecated]] inline bool canRegister() const { return hasRegister(); } [[deprecated]] inline bool canSearch() const { return hasSearch(); } diff --git a/src/xmpp/xmpp-im/xmpp_hash.cpp b/src/xmpp/xmpp-im/xmpp_hash.cpp index decfb0e4..a480823b 100644 --- a/src/xmpp/xmpp-im/xmpp_hash.cpp +++ b/src/xmpp/xmpp-im/xmpp_hash.cpp @@ -41,19 +41,30 @@ struct HashDesc { Hash::Type hashType; const char *const *synonims = nullptr; }; -static const std::array hashTypes { - HashDesc { "unknown", Hash::Type::Unknown }, HashDesc { "sha-1", Hash::Type::Sha1, sha1_synonims }, - HashDesc { "sha-256", Hash::Type::Sha256 }, HashDesc { "sha-512", Hash::Type::Sha512 }, - HashDesc { "sha3-256", Hash::Type::Sha3_256 }, HashDesc { "sha3-512", Hash::Type::Sha3_512 }, - HashDesc { "blake2b-256", Hash::Type::Blake2b256 }, HashDesc { "blake2b-512", Hash::Type::Blake2b512 } -}; +// hash types in priority order mostly by speed +static const std::array hashTypes { + HashDesc { "blake2b-512", Hash::Type::Blake2b512 }, + HashDesc { "blake2b-256", Hash::Type::Blake2b256 }, + HashDesc { "sha-1", Hash::Type::Sha1, sha1_synonims }, + HashDesc { "sha-512", Hash::Type::Sha512 }, + HashDesc { "sha-256", Hash::Type::Sha256 }, + HashDesc { "sha3-512", Hash::Type::Sha3_512 }, + HashDesc { "sha3-256", Hash::Type::Sha3_256 }, +}; // HashDesc { "unknown", Hash::Type::Unknown }, + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) using HashVariant = std::variant; +#else +using HashVariant = std::variant; +#endif HashVariant findHasher(Hash::Type hashType) { QString qcaType; - QCryptographicHash::Algorithm qtType = QCryptographicHash::Algorithm(-1); - Blake2Hash::DigestSize blakeDS = Blake2Hash::DigestSize(-1); + QCryptographicHash::Algorithm qtType = QCryptographicHash::Algorithm(-1); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + Blake2Hash::DigestSize blakeDS = Blake2Hash::DigestSize(-1); +#endif switch (hashType) { case Hash::Type::Sha1: @@ -76,6 +87,7 @@ HashVariant findHasher(Hash::Type hashType) qtType = QCryptographicHash::Sha3_512; qcaType = "sha3_512"; break; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) case Hash::Type::Blake2b256: qcaType = "blake2b_256"; blakeDS = Blake2Hash::Digest256; @@ -84,6 +96,16 @@ HashVariant findHasher(Hash::Type hashType) qcaType = "blake2b_512"; blakeDS = Blake2Hash::Digest512; break; +#else + case Hash::Type::Blake2b256: + qtType = QCryptographicHash::Blake2b_256; + qcaType = "blake2b_256"; + break; + case Hash::Type::Blake2b512: + qtType = QCryptographicHash::Blake2b_512; + qcaType = "blake2b_512"; + break; +#endif case Hash::Type::Unknown: default: qDebug("invalid hash type"); @@ -99,12 +121,15 @@ HashVariant findHasher(Hash::Type hashType) if (qtType != QCryptographicHash::Algorithm(-1)) { return HashVariant { std::in_place_type, qtType }; - } else if (blakeDS != Blake2Hash::DigestSize(-1)) { + } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + else if (blakeDS != Blake2Hash::DigestSize(-1)) { Blake2Hash bh(blakeDS); if (bh.isValid()) { return HashVariant { std::in_place_type, std::move(bh) }; } } +#endif return nullptr; } @@ -123,9 +148,13 @@ Hash::Hash(const QDomElement &el) QString Hash::stringType() const { if (!v_type || int(v_type) > LastType) - return QString(); // must be empty. other code relies on it - static_assert(LastType + 1 == hashTypes.size(), "hashType and enum are not in sync"); - return QLatin1String(hashTypes[int(v_type)].text); + return {}; // must be empty. other code relies on it + static_assert(LastType == hashTypes.size(), "hashType and enum are not in sync"); + auto it = std::ranges::find_if(hashTypes, [this](auto const &v) { return v.hashType == v_type; }); + if (it == hashTypes.end()) { + throw std::logic_error("hashTypes array is inconsistent"); + } + return QLatin1String(it->text); } Hash::Type Hash::parseType(const QStringView &algo) @@ -156,6 +185,7 @@ bool Hash::compute(const QByteArray &ba) std::visit( [&ba, this](auto &&arg) { using T = std::decay_t; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if constexpr (std::is_same_v) { arg.update(ba); v_data = arg.final().toByteArray(); @@ -166,6 +196,16 @@ bool Hash::compute(const QByteArray &ba) if (arg.addData(ba)) v_data = arg.final(); } +#else + // Qt6 claims to have openssl backend and it doesn't copy data (?) + if constexpr (std::is_same_v) { + arg.addData(ba); + v_data = arg.result(); + } else if constexpr (std::is_same_v) { + arg.update(ba); + v_data = arg.final().toByteArray(); + } +#endif }, hasher); @@ -183,6 +223,7 @@ bool Hash::compute(QIODevice *dev) std::visit( [dev, this](auto &&arg) { using T = std::decay_t; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if constexpr (std::is_same_v) { arg.update(dev); v_data = arg.final().toByteArray(); @@ -193,6 +234,15 @@ bool Hash::compute(QIODevice *dev) if (arg.addData(dev)) v_data = arg.final(); } +#else + if constexpr (std::is_same_v) { + arg.addData(dev); + v_data = arg.result(); + } else if constexpr (std::is_same_v) { + arg.update(dev); + v_data = arg.final().toByteArray(); + } +#endif }, hasher); @@ -269,21 +319,13 @@ Hash Hash::from(const QStringView &str) Hash Hash::fastestHash(const Features &features) { - std::array qcaAlgos = { "blake2b_512", "blake2b_256", "sha1", "sha512", "sha256", "sha3_256", "sha3_512" }; - std::array qcaMap = { Blake2b512, Blake2b256, Sha1, Sha512, Sha256, Sha3_256, Sha3_512 }; - QStringList priorityFeatures; - priorityFeatures.reserve(int(qcaAlgos.size())); - for (auto t : qcaMap) { - priorityFeatures.append(QString(QLatin1String("urn:xmpp:hash-function-text-names:")) - + QLatin1String(hashTypes[int(t)].text)); - // REVIEW modify hashTypes with priority info instead? - } - for (int i = 0; i < qcaAlgos.size(); i++) { - if (QCA::isSupported(qcaAlgos[i]) && features.test(priorityFeatures[i])) { - return Hash(qcaMap[i]); + for (auto const &h : hashTypes) { + auto feature = QString(QLatin1String("urn:xmpp:hash-function-text-names:")) + QLatin1String(h.text); + if (features.test(feature)) { + return Hash(h.hashType); } } - return Hash(); // qca is the fastest and it defintiely has sha1. so no reason to use qt or custom blake + return {}; } class StreamHashPrivate { @@ -306,13 +348,22 @@ bool StreamHash::addData(const QByteArray &data) std::visit( [&data, &ret](auto &&arg) { using T = std::decay_t; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if constexpr (std::is_same_v) { arg.update(data); } else if constexpr (std::is_same_v) { arg.addData(data); } else if constexpr (std::is_same_v) { ret = arg.addData(data); - } else + } +#else + if constexpr (std::is_same_v) { + arg.addData(data); + } else if constexpr (std::is_same_v) { + arg.update(data); + } +#endif + else ret = false; }, d->hasher); @@ -324,6 +375,7 @@ Hash StreamHash::final() auto data = std::visit( [](auto &&arg) { using T = std::decay_t; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if constexpr (std::is_same_v) { return arg.final().toByteArray(); } else if constexpr (std::is_same_v) { @@ -331,6 +383,13 @@ Hash StreamHash::final() } else if constexpr (std::is_same_v) { return arg.final(); } +#else + if constexpr (std::is_same_v) { + return arg.result(); + } else if constexpr (std::is_same_v) { + return arg.final().toByteArray(); + } +#endif return QByteArray(); }, d->hasher); diff --git a/src/xmpp/xmpp-im/xmpp_hash.h b/src/xmpp/xmpp-im/xmpp_hash.h index 67400493..b2cb10e9 100644 --- a/src/xmpp/xmpp-im/xmpp_hash.h +++ b/src/xmpp/xmpp-im/xmpp_hash.h @@ -36,7 +36,7 @@ class Features; class Hash { public: - // NB: keep this in sync with Hash::fastestHash() and with hashTypes in cpp the file! + // NB: we have only supported algorithms here. if more is needed then do extra checks enum Type { // XEP-0300 Version 0.5.3 (2018-02-14) Unknown, // not standard, just a default Sha1, // SHOULD NOT diff --git a/src/xmpp/xmpp-im/xmpp_ibb.cpp b/src/xmpp/xmpp-im/xmpp_ibb.cpp index 645c7b95..f2d00510 100644 --- a/src/xmpp/xmpp-im/xmpp_ibb.cpp +++ b/src/xmpp/xmpp-im/xmpp_ibb.cpp @@ -146,7 +146,7 @@ void IBBConnection::close() return; if (d->state == WaitingForAccept) { - d->m->doReject(this, d->iq_id, Stanza::Error::Forbidden, "Rejected"); + d->m->doReject(this, d->iq_id, Stanza::Error::ErrorCond::Forbidden, "Rejected"); resetConnection(); return; } @@ -207,11 +207,11 @@ void IBBConnection::waitForAccept(const Jid &peer, const QString &iq_id, const Q void IBBConnection::takeIncomingData(const IBBData &ibbData) { if (ibbData.seq != d->seq) { - d->m->doReject(this, d->iq_id, Stanza::Error::UnexpectedRequest, "Invalid sequence"); + d->m->doReject(this, d->iq_id, Stanza::Error::ErrorCond::UnexpectedRequest, "Invalid sequence"); return; } if (ibbData.data.size() > d->blockSize) { - d->m->doReject(this, d->iq_id, Stanza::Error::BadRequest, "Too much data"); + d->m->doReject(this, d->iq_id, Stanza::Error::ErrorCond::BadRequest, "Too much data"); return; } d->seq++; @@ -377,7 +377,7 @@ void IBBManager::takeIncomingData(const Jid &from, const QString &id, const IBBD IBBConnection *c = findConnection(data.sid, from); if (!c) { if (sKind == Stanza::IQ) { - d->ibb->respondError(from, id, Stanza::Error::ItemNotFound, "No such stream"); + d->ibb->respondError(from, id, Stanza::Error::ErrorCond::ItemNotFound, "No such stream"); } // TODO imeplement xep-0079 error processing in case of Stanza::Message } else { @@ -392,7 +392,7 @@ void IBBManager::ibb_closeRequest(const Jid &from, const QString &id, const QStr { IBBConnection *c = findConnection(sid, from); if (!c) { - d->ibb->respondError(from, id, Stanza::Error::ItemNotFound, "No such stream"); + d->ibb->respondError(from, id, Stanza::Error::ErrorCond::ItemNotFound, "No such stream"); } else { d->ibb->respondAck(from, id); c->setRemoteClosed(); @@ -490,7 +490,7 @@ void JT_IBB::close(const Jid &to, const QString &sid) void JT_IBB::respondError(const Jid &to, const QString &id, Stanza::Error::ErrorCond cond, const QString &text) { QDomElement iq = createIQ(doc(), "error", to.full(), id); - Stanza::Error error(Stanza::Error::Cancel, cond, text); + Stanza::Error error(Stanza::Error::ErrorType::Cancel, cond, text); iq.appendChild(error.toXml(*client()->doc(), client()->stream().baseNS())); send(iq); } diff --git a/src/xmpp/xmpp-im/xmpp_message.h b/src/xmpp/xmpp-im/xmpp_message.h index 176797c3..2afef8a4 100644 --- a/src/xmpp/xmpp-im/xmpp_message.h +++ b/src/xmpp/xmpp-im/xmpp_message.h @@ -28,6 +28,7 @@ #include "xmpp_url.h" #include +#include class QDateTime; class QString; @@ -49,6 +50,8 @@ typedef enum { OfflineEvent, DeliveredEvent, DisplayedEvent, ComposingEvent, Can class Message { public: + enum class Type { Chat, Error, Groupchat, Headline, Normal }; + // XEP-0334 enum ProcessingHint { NoPermanentStore = 1, NoStore = 2, NoCopy = 4, Store = 8 }; Q_DECLARE_FLAGS(ProcessingHints, ProcessingHint) @@ -58,6 +61,11 @@ class Message { QString id; }; + struct Reactions { + QString targetId; + QSet reactions; + }; + Message(); Message(const Jid &to); Message(const Message &from); @@ -70,7 +78,8 @@ class Message { Jid to() const; Jid from() const; QString id() const; - QString type() const; + Type type() const; + QString typeStr() const; QString lang() const; QString subject(const QString &lang = {}) const; QString subject(const QLocale &lang) const; @@ -83,7 +92,7 @@ class Message { void setTo(const Jid &j); void setFrom(const Jid &j); void setId(const QString &s); - void setType(const QString &s); + void setType(Type type); void setLang(const QString &s); void setSubject(const QString &s, const QString &lang = {}); void setBody(const QString &s, const QString &lang = {}); @@ -212,6 +221,13 @@ class Message { void addReference(const Reference &r); void setReferences(const QList &r); + // XEP-0444 message reaction + void setReactions(const Reactions &reactions); + Reactions reactions() const; + + void setRetraction(const QString &retractedMessageId); + QString retraction() const; + // Obsolete invitation QString invite() const; void setInvite(const QString &s); diff --git a/src/xmpp/xmpp-im/xmpp_serverinfomanager.cpp b/src/xmpp/xmpp-im/xmpp_serverinfomanager.cpp index 5db2e3af..d592f39c 100644 --- a/src/xmpp/xmpp-im/xmpp_serverinfomanager.cpp +++ b/src/xmpp/xmpp-im/xmpp_serverinfomanager.cpp @@ -43,12 +43,21 @@ void ServerInfoManager::reset() void ServerInfoManager::initialize() { - connect(_client, SIGNAL(disconnected()), SLOT(deinitialize())); - JT_DiscoInfo *jt = new JT_DiscoInfo(_client->rootTask()); - connect(jt, SIGNAL(finished()), SLOT(disco_finished())); - jt->get(_client->jid().domain()); - jt->go(true); + connect(_client, &XMPP::Client::disconnected, this, &ServerInfoManager::deinitialize); + { + JT_DiscoInfo *jt = new JT_DiscoInfo(_client->rootTask()); + connect(jt, &JT_DiscoInfo::finished, this, &ServerInfoManager::server_disco_finished); + jt->get(_client->jid().domain()); + jt->go(true); + } + + { + JT_DiscoInfo *jt = new JT_DiscoInfo(_client->rootTask()); + connect(jt, &JT_DiscoInfo::finished, this, &ServerInfoManager::account_disco_finished); + jt->get(_client->jid().bare()); + jt->go(true); + } queryServicesList(); } @@ -60,8 +69,6 @@ void ServerInfoManager::deinitialize() const QString &ServerInfoManager::multicastService() const { return _multicastService; } -bool ServerInfoManager::hasPEP() const { return _hasPEP; } - bool ServerInfoManager::canMessageCarbons() const { return _canMessageCarbons; } void ServerInfoManager::queryServicesList() @@ -70,7 +77,7 @@ void ServerInfoManager::queryServicesList() auto jtitems = new JT_DiscoItems(_client->rootTask()); connect( jtitems, &JT_DiscoItems::finished, this, - [=]() { + [this, jtitems]() { _servicesInfo.clear(); // if (jtitems->success()) { _servicesListState = ST_Ready; @@ -87,6 +94,14 @@ void ServerInfoManager::queryServicesList() jtitems->go(true); } +void ServerInfoManager::finish(ServiceInfoQuery *q, const QList &items) +{ + emit q->finished(items); + if (q->parent() == this) { + q->deleteLater(); + } +} + void ServerInfoManager::checkPendingServiceQueries() { // if services list is not ready yet we have to exit. if it's failed we have to finish all pending queries @@ -95,17 +110,18 @@ void ServerInfoManager::checkPendingServiceQueries() const auto sqs = _serviceQueries; _serviceQueries.clear(); for (const auto &q : sqs) { - q.callback(QList()); + finish(q); } } return; } // services list is ready here and we can start checking it and sending disco#info to not cached entries - auto sqIt = _serviceQueries.begin(); - while (sqIt != _serviceQueries.end()) { + auto queryIt = _serviceQueries.begin(); + while (queryIt != _serviceQueries.end()) { // populate services to query for this service request + auto sqIt = *queryIt; if (!sqIt->servicesToQueryDefined) { sqIt->spareServicesToQuery.clear(); // grep all suitble service jids. moving forward preferred ones @@ -115,7 +131,7 @@ void ServerInfoManager::checkPendingServiceQueries() if (sqIt->nameHint.isValid()) { if (!sqIt->nameHint.isValid() || sqIt->nameHint.match(si.key()).hasMatch()) { sqIt->servicesToQuery.push_back(si.key()); - } else if (sqIt->options & SQ_CheckAllOnNoMatch) { + } else if (sqIt->options & ServiceInfoQuery::CheckAllOnNoMatch) { sqIt->spareServicesToQuery.push_back(si.key()); } } else { @@ -127,8 +143,8 @@ void ServerInfoManager::checkPendingServiceQueries() sqIt->spareServicesToQuery.clear(); } if (sqIt->servicesToQuery.empty()) { - sqIt->callback(QList()); - _serviceQueries.erase(sqIt++); + finish(sqIt); + _serviceQueries.erase(queryIt++); continue; } sqIt->servicesToQueryDefined = true; @@ -162,7 +178,7 @@ void ServerInfoManager::checkPendingServiceQueries() sqIt->features.constBegin(), sqIt->features.constEnd(), false, [&si](bool a, const QSet &b) { return a || si->item.features().test(b); }))) { sqIt->result.append(si->item); - if (sqIt->options & SQ_FinishOnFirstMatch) { + if (sqIt->options & ServiceInfoQuery::FinishOnFirstMatch) { break; } } @@ -203,20 +219,19 @@ void ServerInfoManager::checkPendingServiceQueries() } // if has at least one sufficient result - auto forceFinish = (!sqIt->result.isEmpty() && (sqIt->options & SQ_FinishOnFirstMatch)); // stop on first found + auto forceFinish = (!sqIt->result.isEmpty() + && (sqIt->options & ServiceInfoQuery::FinishOnFirstMatch)); // stop on first found // if nothing in progress then we have full result set or nothing found even in spare list if (forceFinish || !hasInProgress) { // self explanatory - auto callback = std::move(sqIt->callback); - auto result = sqIt->result; - _serviceQueries.erase(sqIt++); - callback(result); + _serviceQueries.erase(queryIt++); + finish(sqIt, sqIt->result); } else { - ++sqIt; + ++queryIt; } } } -void ServerInfoManager::appendQuery(const ServiceQuery &q) +void ServerInfoManager::appendQuery(ServiceInfoQuery *q) { _serviceQueries.push_back(q); if (_servicesListState == ST_InProgress) { @@ -229,11 +244,14 @@ void ServerInfoManager::appendQuery(const ServiceQuery &q) } } -void ServerInfoManager::queryServiceInfo(const QString &category, const QString &type, - const QList> &features, const QRegularExpression &nameHint, - SQOptions options, std::function &items)> callback) +ServiceInfoQuery *ServerInfoManager::queryServiceInfo(const QString &category, const QString &type, + const QList> &features, + const QRegularExpression &nameHint, + ServiceInfoQuery::Options options) { - appendQuery(ServiceQuery(type, category, features, nameHint, options, std::move(callback))); + auto query = new ServiceInfoQuery(type, category, features, nameHint, options, this); + appendQuery(query); + return query; } void ServerInfoManager::setServiceMeta(const Jid &service, const QString &key, const QVariant &value) @@ -253,23 +271,16 @@ QVariant ServerInfoManager::serviceMeta(const Jid &service, const QString &key) return QVariant(); } -void ServerInfoManager::disco_finished() +void ServerInfoManager::server_disco_finished() { JT_DiscoInfo *jt = static_cast(sender()); if (jt->success()) { - _features = jt->item().features(); + _serverFeatures = jt->item().features(); - if (_features.hasMulticast()) + if (_serverFeatures.hasMulticast()) _multicastService = _client->jid().domain(); - _canMessageCarbons = _features.hasMessageCarbons(); - - // Identities - DiscoItem::Identities is = jt->item().identities(); - for (const DiscoItem::Identity &i : is) { - if (i.category == "pubsub" && i.type == "pep") - _hasPEP = true; - } + _canMessageCarbons = _serverFeatures.hasMessageCarbons(); auto servInfo = jt->item().findExtension(XData::Data_Result, QLatin1String("http://jabber.org/network/serverinfo")); @@ -284,4 +295,22 @@ void ServerInfoManager::disco_finished() emit featuresChanged(); } } + +void ServerInfoManager::account_disco_finished() +{ + JT_DiscoInfo *jt = static_cast(sender()); + if (jt->success()) { + // Identities + DiscoItem::Identities is = jt->item().identities(); + for (const DiscoItem::Identity &i : is) { + if (i.category == "pubsub" && i.type == "pep") { + _hasPEP = true; + } + } + _hasPersistentStorage = jt->item().hasPersistentStorage(); + _accountFeatures = jt->item().features(); + + emit featuresChanged(); + } +} } // namespace XMPP diff --git a/src/xmpp/xmpp-im/xmpp_serverinfomanager.h b/src/xmpp/xmpp-im/xmpp_serverinfomanager.h index 8a6a795a..0b0ead20 100644 --- a/src/xmpp/xmpp-im/xmpp_serverinfomanager.h +++ b/src/xmpp/xmpp-im/xmpp_serverinfomanager.h @@ -28,7 +28,6 @@ #include #include -#include #include namespace XMPP { @@ -36,37 +35,43 @@ class Client; class Features; class Jid; -class ServerInfoManager : public QObject { +class ServiceInfoQuery : public QObject { Q_OBJECT public: - enum SQOption { - SQ_CheckAllOnNoMatch = 1, // check all if matched by name services do not match or no matched by name - SQ_FinishOnFirstMatch = 2, // first callback is final - SQ_CallbackOnAnyMatches = 4 // TODO don't wait while all services will be discovered. empty result list = final + enum Option { + CheckAllOnNoMatch = 1, // check all if matched by name services do not match or no matched by name + FinishOnFirstMatch = 2, // first callback is final + CallbackOnAnyMatches = 4 // TODO don't wait while all services will be discovered. empty result list = final }; - Q_DECLARE_FLAGS(SQOptions, SQOption) + Q_DECLARE_FLAGS(Options, Option) + + QList result; private: - struct ServiceQuery { - const QString type; - const QString category; - const QList> features; - const QRegularExpression nameHint; - const SQOptions options; - const std::function &item)> callback; - std::list servicesToQuery; - std::list spareServicesToQuery; // usually a fallback when the above is not matched - bool servicesToQueryDefined = false; - QList result; - - ServiceQuery(const QString &type, const QString &category, const QList> &features, - const QRegularExpression &nameHint, const SQOptions &options, - const std::function &item)> &&callback) : - type(type), category(category), features(features), nameHint(nameHint), options(options), callback(callback) - { - } - }; + friend class ServerInfoManager; + const QString type; + const QString category; + const QList> features; + const QRegularExpression nameHint; + const Options options; + std::list servicesToQuery; + std::list spareServicesToQuery; // usually a fallback when the above is not matched + bool servicesToQueryDefined = false; + + ServiceInfoQuery(const QString &type, const QString &category, const QList> &features, + const QRegularExpression &nameHint, const Options &options, QObject *parent) : + QObject(parent), type(type), category(category), features(features), nameHint(nameHint), options(options) + { + } + +signals: + void finished(const QList &item); +}; +class ServerInfoManager : public QObject { + Q_OBJECT +public: +private: enum ServicesState { ST_NotQueried, ST_InProgress, ST_Ready, ST_Failed }; struct ServiceInfo { @@ -79,8 +84,10 @@ class ServerInfoManager : public QObject { ServerInfoManager(XMPP::Client *client); const QString &multicastService() const; - bool hasPEP() const; - inline const Features &features() const { return _features; } + inline bool hasPEP() const { return _hasPEP; } + inline bool hasPersistentStorage() const { return _hasPersistentStorage; } + inline const Features &serverFeatures() const { return _serverFeatures; } + inline const Features &accountFeatures() const { return _accountFeatures; } bool canMessageCarbons() const; inline const QMap &extraServerInfo() const { return _extraServerInfo; } @@ -101,18 +108,19 @@ class ServerInfoManager : public QObject { nameHint = (http\..*|) // search for service name like http.jabber.ru Result: disco info for upload.jabber.ru will be returned. */ - void queryServiceInfo(const QString &category, const QString &type, const QList> &features, - const QRegularExpression &nameHint, SQOptions options, - std::function &items)> callback); - void setServiceMeta(const Jid &service, const QString &key, const QVariant &value); - QVariant serviceMeta(const Jid &service, const QString &key); + ServiceInfoQuery *queryServiceInfo(const QString &category, const QString &type, + const QList> &features, const QRegularExpression &nameHint, + ServiceInfoQuery::Options options); + void setServiceMeta(const Jid &service, const QString &key, const QVariant &value); + QVariant serviceMeta(const Jid &service, const QString &key); signals: void featuresChanged(); void servicesChanged(); private slots: - void disco_finished(); + void server_disco_finished(); + void account_disco_finished(); void initialize(); void deinitialize(); void reset(); @@ -120,23 +128,26 @@ private slots: private: void queryServicesList(); void checkPendingServiceQueries(); - void appendQuery(const ServiceQuery &q); + void appendQuery(ServiceInfoQuery *q); + void finish(ServiceInfoQuery *q, const QList &items = {}); private: XMPP::Client *_client = nullptr; CapsSpec _caps; - Features _features; + Features _serverFeatures; + Features _accountFeatures; QString _multicastService; QMap _extraServerInfo; // XEP-0128, XEP-0157 - std::list _serviceQueries; // a storage of pending requests as result of `queryService` call - ServicesState _servicesListState = ST_NotQueried; + std::list _serviceQueries; // a storage of pending requests as result of `queryService` call + ServicesState _servicesListState = ST_NotQueried; QMap _servicesInfo; // all the diso#info requests for services of this server jid=>(state,info) - bool _featuresRequested; - bool _hasPEP; - bool _canMessageCarbons; + bool _featuresRequested = false; + bool _hasPEP = false; + bool _hasPersistentStorage = false; + bool _canMessageCarbons = false; }; } // namespace XMPP diff --git a/src/xmpp/xmpp-im/xmpp_status.h b/src/xmpp/xmpp-im/xmpp_status.h index 17c649cb..6def813e 100644 --- a/src/xmpp/xmpp-im/xmpp_status.h +++ b/src/xmpp/xmpp-im/xmpp_status.h @@ -29,6 +29,8 @@ #include #include +#include + namespace XMPP { class DiscoItem; class StatusPrivate; @@ -132,9 +134,8 @@ class Status { void setSongTitle(const QString &); // XEP-0153: vCard-Based Avatars - const QByteArray &photoHash() const; - void setPhotoHash(const QByteArray &); - bool hasPhotoHash() const; + const std::optional &photoHash() const; + void setPhotoHash(const QByteArray &); // XEP-0231 bits of binary void addBoBData(const BoBData &); diff --git a/src/xmpp/xmpp-im/xmpp_task.cpp b/src/xmpp/xmpp-im/xmpp_task.cpp index bdd7c2db..7c4c2a02 100644 --- a/src/xmpp/xmpp-im/xmpp_task.cpp +++ b/src/xmpp/xmpp-im/xmpp_task.cpp @@ -268,7 +268,7 @@ bool Task::iqVerify(const QDomElement &x, const Jid &to, const QString &id, cons // empty 'from' ? if (from.isEmpty()) { // allowed if we are querying the server - if (!to.isEmpty() && !to.compare(server)) + if (!to.isEmpty() && !to.compare(local, false) && !to.compare(server)) return false; } // from ourself? diff --git a/src/xmpp/xmpp-im/xmpp_tasks.cpp b/src/xmpp/xmpp-im/xmpp_tasks.cpp index bef94b4b..90d8232c 100644 --- a/src/xmpp/xmpp-im/xmpp_tasks.cpp +++ b/src/xmpp/xmpp-im/xmpp_tasks.cpp @@ -614,9 +614,9 @@ void JT_Presence::pres(const Status &s) tag.appendChild(m); } - if (s.hasPhotoHash()) { + if (s.photoHash().has_value()) { QDomElement m = doc()->createElementNS("vcard-temp:x:update", "x"); - m.appendChild(textTag(doc(), "photo", QString::fromLatin1(s.photoHash().toHex()))); + m.appendChild(textTag(doc(), "photo", QString::fromLatin1(s.photoHash()->toHex()))); tag.appendChild(m); } @@ -755,7 +755,7 @@ bool JT_PushPresence::take(const QDomElement &e) if (!t.isNull()) p.setPhotoHash( QByteArray::fromHex(tagContent(t).toLatin1())); // if hash is empty this may mean photo removal - // else vcard.hasPhotoHash() returns false and that's mean user is not yet ready to advertise his image + // else vcard.photoHash() returns false and that's mean user is not yet ready to advertise his image } else if (i.tagName() == "x" && i.namespaceURI() == "http://jabber.org/protocol/muc#user") { for (QDomElement muc_e = i.firstChildElement(); !muc_e.isNull(); muc_e = muc_e.nextSiblingElement()) { if (muc_e.tagName() == "item") @@ -790,43 +790,6 @@ bool JT_PushPresence::take(const QDomElement &e) //---------------------------------------------------------------------------- // JT_Message //---------------------------------------------------------------------------- -static QDomElement oldStyleNS(const QDomElement &e) -{ - // find closest parent with a namespace - QDomNode par = e.parentNode(); - while (!par.isNull() && par.namespaceURI().isNull()) - par = par.parentNode(); - bool noShowNS = false; - if (!par.isNull() && par.namespaceURI() == e.namespaceURI()) - noShowNS = true; - - QDomElement i; - int x; - // if(noShowNS) - i = e.ownerDocument().createElement(e.tagName()); - // else - // i = e.ownerDocument().createElementNS(e.namespaceURI(), e.tagName()); - - // copy attributes - QDomNamedNodeMap al = e.attributes(); - for (x = 0; x < al.count(); ++x) - i.setAttributeNode(al.item(x).cloneNode().toAttr()); - - if (!noShowNS) - i.setAttribute("xmlns", e.namespaceURI()); - - // copy children - QDomNodeList nl = e.childNodes(); - for (x = 0; x < nl.count(); ++x) { - QDomNode n = nl.item(x); - if (n.isElement()) - i.appendChild(oldStyleNS(n.toElement())); - else - i.appendChild(n.cloneNode()); - } - return i; -} - JT_Message::JT_Message(Task *parent, Message &msg) : Task(parent), m(msg) { if (msg.id().isEmpty()) @@ -839,7 +802,7 @@ void JT_Message::onGo() { Stanza s = m.toStanza(&(client()->stream())); - QDomElement e = s.element(); // oldStyleNS(s.element()); + QDomElement e = s.element(); if (auto encryptionHandler = client()->encryptionHandler()) { Q_UNUSED(encryptionHandler->encryptMessageElement(e)); @@ -1702,7 +1665,7 @@ bool JT_BoBServer::take(const QDomElement &e) BoBData bd = client()->bobManager()->bobData(data.attribute("cid")); if (bd.isNull()) { iq = createIQ(client()->doc(), "error", e.attribute("from"), e.attribute("id")); - Stanza::Error error(Stanza::Error::Cancel, Stanza::Error::ItemNotFound); + Stanza::Error error(Stanza::Error::ErrorType::Cancel, Stanza::Error::ErrorCond::ItemNotFound); iq.appendChild(error.toXml(*doc(), client()->stream().baseNS())); } else { iq = createIQ(doc(), "result", e.attribute("from"), e.attribute("id")); @@ -1877,12 +1840,12 @@ bool JT_CaptchaChallenger::take(const QDomElement &x) } else { Stanza::Error::ErrorCond ec; if (r == CaptchaChallenge::Unavailable) { - ec = Stanza::Error::ServiceUnavailable; + ec = Stanza::Error::ErrorCond::ServiceUnavailable; } else { - ec = Stanza::Error::NotAcceptable; + ec = Stanza::Error::ErrorCond::NotAcceptable; } iq = createIQ(doc(), "error", d->j.full(), rid); - Stanza::Error error(Stanza::Error::Cancel, ec); + Stanza::Error error(Stanza::Error::ErrorType::Cancel, ec); iq.appendChild(error.toXml(*doc(), client()->stream().baseNS())); } send(iq); diff --git a/src/xmpp/xmpp-im/xmpp_vcard4.cpp b/src/xmpp/xmpp-im/xmpp_vcard4.cpp new file mode 100644 index 00000000..35e30d8c --- /dev/null +++ b/src/xmpp/xmpp-im/xmpp_vcard4.cpp @@ -0,0 +1,1591 @@ +/* + * xmpp_vcard4.cpp - classes for handling vCards according to rfc6351 + * Copyright (C) 2024 Sergei Ilinykh + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + * + */ + +#include "xmpp_vcard4.h" + +#include "xmpp_vcard.h" + +#include +#include +#include +#include +#include +#include + +namespace XMPP::VCard4 { + +namespace { + +#define INIT_D() \ + do { \ + if (!d) \ + d = new VCardData; \ + } while (0) + + const QString VCARD_NAMESPACE = QLatin1String("urn:ietf:params:xml:ns:vcard-4.0"); + + struct VCardHelper { + static QString extractText(const QDomElement &element, const char *tagName) + { + QDomElement tagElement = element.firstChildElement(QString::fromLatin1(tagName)); + return tagElement.isNull() ? QString() : tagElement.text(); + } + + static QStringList extractTexts(const QDomElement &element, const char *tagName) + { + auto tn = QString::fromLatin1(tagName); + QStringList texts; + QDomElement tagElement = element.firstChildElement(tn); + while (!tagElement.isNull()) { + texts.append(tagElement.text()); + tagElement = tagElement.nextSiblingElement(tn); + } + return texts; + } + + static PStrings parseParameterList(const QDomElement &element, const QString &tagName, + const QString &innetTagName) + { + PStrings result; + QDomElement tagElement = element.firstChildElement(tagName); + while (!tagElement.isNull()) { + Parameters parameters(tagElement.firstChildElement(QLatin1String("parameters"))); + QString value = tagElement.firstChildElement(innetTagName).text(); + result.append({ parameters, value }); + tagElement = tagElement.nextSiblingElement(tagName); + } + return result; + } + + static void serialize(QDomElement parent, const PHistorical &historical, const char *tagName) + { + std::visit( + [&](auto const &v) mutable { + if (v.isNull()) { + return; + } + auto doc = parent.ownerDocument(); + auto bday = parent.appendChild(doc.createElement(QLatin1String(tagName))).toElement(); + historical.parameters.addTo(bday); + using Tv = std::decay_t; + if constexpr (std::is_same_v) { + VCardHelper::addTextElement(doc, bday, QLatin1String("text"), QStringList { v }); + } else if constexpr (std::is_same_v) { + VCardHelper::addTextElement(doc, bday, QLatin1String("date"), + QStringList { v.toString(Qt::ISODate) }); + } else if constexpr (std::is_same_v) { + VCardHelper::addTextElement(doc, bday, QLatin1String("date-time"), + QStringList { v.toString(Qt::ISODate) }); + } else if constexpr (std::is_same_v) { + VCardHelper::addTextElement(doc, bday, QLatin1String("time"), + QStringList { v.toString(Qt::ISODate) }); + } + }, + historical.data); + } + + template + static void addTextElement(QDomDocument &document, QDomElement &parent, const QString &tagName, + const ListType &texts) + { + if (!texts.isEmpty()) { + QDomElement element = document.createElement(tagName); + for (const auto &text : texts) { + QDomElement textElement = element; + textElement.appendChild(document.createTextNode(text)); + } + parent.appendChild(element); + } + } + + template + static void serializeList(QDomElement &parent, const TaggedList> &list, const QString &tagName, + const QString &innerTagName = QLatin1String("text")) + { + if (list.isEmpty()) { + return; + } + auto document = parent.ownerDocument(); + for (const auto &entry : list) { + QDomElement element = document.createElement(tagName); + entry.parameters.addTo(element); + if constexpr (std::is_same_v) { + addTextElement(document, element, QLatin1String("text"), QStringList { entry.data }); + } else if constexpr (std::is_same_v || std::is_same_v) { + addTextElement(document, element, QLatin1String("uri"), QStringList { entry.data.toString() }); + } else if constexpr (std::is_same_v) { + std::visit( + [&](auto v) { + using Tv = std::decay_t; + if constexpr (std::is_same_v) { + addTextElement(document, element, QLatin1String("uri"), QStringList { v.toString() }); + } else { + addTextElement(document, element, QLatin1String("text"), QStringList { v }); + } + }, + entry.data); + } else if constexpr (std::is_same_v) { + for (auto const &s : entry.data) { + element.appendChild(document.createElement(QLatin1String("text"))) + .appendChild(document.createTextNode(s)); + } + } else if constexpr (std::is_same_v) { + std::visit( + [&](auto v) { + using Tv = std::decay_t; + if constexpr (std::is_same_v) { + addTextElement(document, element, QLatin1String("uri"), QStringList { v.toString() }); + } else if constexpr (std::is_same_v) { + addTextElement( + document, element, QLatin1String("utc-offset"), + QStringList { + QLatin1Char(v < 0 ? '-' : '+') + + QString("%1%2") + .arg(std::abs(v) / 3600, 2, 10, QChar::fromLatin1('0')) + .arg((std::abs(v) % 3600) / 60, 2, 10, QChar::fromLatin1('0')) }); + } else { + addTextElement(document, element, QLatin1String("text"), QStringList { v }); + } + }, + entry.data); + } else { + throw std::logic_error("should never happen. some type is not supported"); + } + parent.appendChild(element); + } + } + + static VCard4::Gender stringToGender(const QString &genderStr) + { + if (genderStr.compare(QLatin1String("M"), Qt::CaseInsensitive) == 0) { + return Gender::Male; + } else if (genderStr.compare(QLatin1String("F"), Qt::CaseInsensitive) == 0) { + return Gender::Female; + } else if (genderStr.compare(QLatin1String("O"), Qt::CaseInsensitive) == 0) { + return Gender::Other; + } else if (genderStr.compare(QLatin1String("N"), Qt::CaseInsensitive) == 0) { + return Gender::None; + } else if (genderStr.compare(QLatin1String("U"), Qt::CaseInsensitive) == 0) { + return Gender::Unknown; + } else { + return Gender::Undefined; + } + } + + static QString genderToString(Gender gender) + { + switch (gender) { + case Gender::Male: + return QLatin1String("M"); + case Gender::Female: + return QLatin1String("F"); + case Gender::Other: + return QLatin1String("O"); + case Gender::None: + return QLatin1String("N"); + case Gender::Unknown: + return QLatin1String("U"); + default: + return QLatin1String(""); + } + } + + template + static void fillContainer(QDomElement parent, const char *tagName, TaggedList> &container) + { + auto tn = QString::fromLatin1(tagName); + for (auto e = parent.firstChildElement(tn); !e.isNull(); e = e.nextSiblingElement(tn)) { + Parameters parameters(e.firstChildElement(QLatin1String("parameters"))); + if constexpr (std::is_same_v) { + container.append({ parameters, VCardHelper::extractText(e, "text") }); + } else if constexpr (std::is_same_v || std::is_same_v) { + container.append({ parameters, ItemT(VCardHelper::extractText(e, "uri")) }); + } else if constexpr (std::is_same_v) { + auto uri = VCardHelper::extractText(e, "uri"); + if (uri.isEmpty()) { + auto text = VCardHelper::extractText(e, "text"); + container.append({ parameters, text }); + } else { + container.append({ parameters, QUrl(uri) }); + } + } else if constexpr (std::is_same_v) { + container.append({ parameters, VCardHelper::extractTexts(e, "text") }); + } else if constexpr (std::is_same_v) { + auto text = VCardHelper::extractText(e, "text"); + if (text.isEmpty()) { + auto uri = VCardHelper::extractText(e, "uri"); + if (uri.isEmpty()) { + auto offset = VCardHelper::extractText(e, "utc-offset"); + bool neg = offset.startsWith(QLatin1Char('-')); + if (neg || offset.startsWith(QLatin1Char('+'))) { + QStringView sv { offset }; + container.append( + { parameters, + (sv.mid(1, 2).toInt() * 3600 + sv.mid(3, 2).toInt() * 60) * (neg ? 1 : -1) }); + } else { + container.append({ parameters, 0 }); + } + } else { + container.append({ parameters, QUrl(uri) }); + } + } else { + container.append({ parameters, text }); + } + } + } + }; + + static void unserialize(const QDomElement &parent, const char *tagName, PHistorical &to) + { + QDomElement source = parent.firstChildElement(QLatin1String(tagName)); + if (source.isNull()) { + return; + } + to.parameters = Parameters(source.firstChildElement(QLatin1String("parameters"))); + auto v = VCardHelper::extractText(source, "date"); + if (v.isNull()) { + v = VCardHelper::extractText(source, "date-time"); + if (v.isNull()) { + v = VCardHelper::extractText(source, "time"); + if (v.isNull()) { + to.data = VCardHelper::extractText(source, "text"); + } else { + to.data = QTime::fromString(v, Qt::ISODate); + } + } else { + to.data = QDateTime::fromString(v, Qt::ISODate); + } + } else { + to.data = QDate::fromString(v, Qt::ISODate); + } + } + + static bool isNull(const PHistorical &h) + { + return std::visit([](auto const &v) { return v.isNull(); }, h.data); + } + }; + +} // namespace + +Names::Names(const QDomElement &element) +{ + surname = VCardHelper::extractTexts(element, "surname"); + given = VCardHelper::extractTexts(element, "given"); + additional = VCardHelper::extractTexts(element, "additional"); + prefix = VCardHelper::extractTexts(element, "prefix"); + suffix = VCardHelper::extractTexts(element, "suffix"); +} + +QDomElement Names::toXmlElement(QDomDocument &document) const +{ + QDomElement nameElement = document.createElement(QLatin1String("n")); + VCardHelper::addTextElement(document, nameElement, QLatin1String("surname"), surname); + VCardHelper::addTextElement(document, nameElement, QLatin1String("given"), given); + VCardHelper::addTextElement(document, nameElement, QLatin1String("additional"), additional); + VCardHelper::addTextElement(document, nameElement, QLatin1String("prefix"), prefix); + VCardHelper::addTextElement(document, nameElement, QLatin1String("suffix"), suffix); + return nameElement; +} + +bool Names::isEmpty() const noexcept +{ + return surname.isEmpty() && given.isEmpty() && additional.isEmpty() && prefix.isEmpty() && suffix.isEmpty(); +} + +Address::Address(const QDomElement &element) +{ + pobox = VCardHelper::extractTexts(element, "pobox"); + extaddr = VCardHelper::extractTexts(element, "ext"); + street = VCardHelper::extractTexts(element, "street"); + locality = VCardHelper::extractTexts(element, "locality"); + region = VCardHelper::extractTexts(element, "region"); + code = VCardHelper::extractTexts(element, "code"); + country = VCardHelper::extractTexts(element, "country"); +} + +QDomElement Address::toXmlElement(QDomDocument &document) const +{ + if (isEmpty()) { + return {}; + } + QDomElement addressElement = document.createElement(QLatin1String("adr")); + VCardHelper::addTextElement(document, addressElement, QLatin1String("pobox"), pobox); + VCardHelper::addTextElement(document, addressElement, QLatin1String("ext"), extaddr); + VCardHelper::addTextElement(document, addressElement, QLatin1String("street"), street); + VCardHelper::addTextElement(document, addressElement, QLatin1String("locality"), locality); + VCardHelper::addTextElement(document, addressElement, QLatin1String("region"), region); + VCardHelper::addTextElement(document, addressElement, QLatin1String("code"), code); + VCardHelper::addTextElement(document, addressElement, QLatin1String("country"), country); + return addressElement; +} + +bool Address::isEmpty() const noexcept +{ + return pobox.isEmpty() && extaddr.isEmpty() && street.isEmpty() && locality.isEmpty() && region.isEmpty() + && code.isEmpty() && country.isEmpty(); +} + +Parameters::Parameters(const QDomElement &element) +{ + if (element.isNull()) { + return; + } + + language = element.firstChildElement("language").text(); + altid = element.firstChildElement("altid").text(); + + QDomElement pidElement = element.firstChildElement("pid"); + while (!pidElement.isNull()) { + pid.append(pidElement.text()); + pidElement = pidElement.nextSiblingElement("pid"); + } + + QDomElement prefElement = element.firstChildElement("pref"); + if (!prefElement.isNull()) { + pref = prefElement.text().toInt(); + } + + QDomElement typeElement = element.firstChildElement("type"); + while (!typeElement.isNull()) { + QDomElement textElement = typeElement.firstChildElement("text"); + while (!textElement.isNull()) { + type.append(textElement.text()); + textElement = textElement.nextSiblingElement("text"); + } + typeElement = typeElement.nextSiblingElement("type"); + } + + geo = element.firstChildElement("geo").text(); + tz = element.firstChildElement("tz").text(); + label = element.firstChildElement("label").text(); +} + +void Parameters::addTo(QDomElement parent) const +{ + QDomDocument document = parent.ownerDocument(); + + auto pel = document.createElement(QLatin1String("parameters")); + + if (!language.isEmpty()) { + QDomElement element = document.createElement("language"); + element.appendChild(document.createTextNode(language)); + pel.appendChild(element); + } + + if (!altid.isEmpty()) { + QDomElement element = document.createElement("altid"); + element.appendChild(document.createTextNode(altid)); + pel.appendChild(element); + } + + for (auto const &value : pid) { + QDomElement element = document.createElement("pid"); + element.appendChild(document.createTextNode(value)); + pel.appendChild(element); + } + + if (pref > 0) { + QDomElement element = document.createElement("pref"); + element.appendChild(document.createTextNode(QString::number(pref))); + pel.appendChild(element); + } + + if (!type.isEmpty()) { + QDomElement typeElement = document.createElement("type"); + for (const QString &value : type) { + QDomElement textElement = document.createElement("text"); + textElement.appendChild(document.createTextNode(value)); + typeElement.appendChild(textElement); + } + pel.appendChild(typeElement); + } + + if (!geo.isEmpty()) { + QDomElement element = document.createElement("geo"); + element.appendChild(document.createTextNode(geo)); + pel.appendChild(element); + } + + if (!tz.isEmpty()) { + QDomElement element = document.createElement("tz"); + element.appendChild(document.createTextNode(tz)); + pel.appendChild(element); + } + + if (!label.isEmpty()) { + QDomElement element = document.createElement("label"); + element.appendChild(document.createTextNode(label)); + pel.appendChild(element); + } + + if (pel.hasChildNodes()) { + parent.appendChild(pel); + } +} + +bool Parameters::isEmpty() const +{ + return language.isEmpty() && altid.isEmpty() && pid.isEmpty() && pref == -1 && type.isEmpty() && geo.isEmpty() + && tz.isEmpty() && label.isEmpty(); +} + +UriValue::UriValue(const QString &uri) +{ + if (uri.startsWith(QLatin1String("data:"))) { + static QRegularExpression re(QLatin1String("data:([^;]+);base64,(.*)"), + QRegularExpression::MultilineOption + | QRegularExpression::DotMatchesEverythingOption); + QRegularExpressionMatch match = re.match(uri); + if (match.hasMatch()) { + mediaType = match.captured(1); + data = QByteArray::fromBase64(match.captured(2).trimmed().toUtf8()); + } + } else { + url = QUrl(uri); + } +} + +UriValue::UriValue(const QByteArray &data, const QString &mime) : data(data), mediaType(mime) { } + +QString UriValue::toString() const +{ + if (!mediaType.isEmpty()) { + return QString(QLatin1String("data:%1;base64,%2")).arg(mediaType, QString(data.toBase64())); + } else { + return url.toString(); + } +} + +class VCard::VCardData : public QSharedData { +public: + PUris source; // any nuumber of uris + QString kind; // none is ok + + // Identification Properties + PStrings fullName; // at least one full name + PNames names; // at most 1 + PStringLists nickname; + PAdvUris photo; + PHistorical bday; // at most 1 + PHistorical anniversary; // at most 1 + VCard4::Gender gender = Gender::Undefined; + QString genderComment; + + // Delivery Addressing Properties + PAddresses addresses; // any number of addresses + + // Communications Properties + PUrisOrTexts tels; // any number of telephones + PStrings emails; // any number of emails + PUris impp; // any number impp + PStrings lang; // any number of languages + + // Geographical Properties + PTimeZones timeZone; // any number of time zones + PUris geo; // any number of geo locations + + // Organizational Properties + PStrings title; + PStrings role; + PAdvUris logo; + PStringLists org; + PUris member; + PUrisOrTexts related; + + // Explanatory Properties + PStringLists categories; + PStrings note; + QString prodid; // at most 1 + QDateTime rev; // at most 1 + PAdvUris sound; + QString uid; // at most 1 + QHash clientPidMap; + PUris urls; + PUrisOrTexts key; + + // Calendar Properties + PUris busyTimeUrl; + PUris calendarRequestUri; + PUris calendarUri; + + VCardData() { } + + VCardData(const QDomElement &element) + { + if (element.namespaceURI() != VCARD_NAMESPACE) + return; + + auto foreachElement = [](auto parent, auto tagName, auto callback) { + for (auto e = parent.firstChildElement(tagName); !e.isNull(); e = e.nextSiblingElement(tagName)) { + callback(e); + } + }; + + QDomElement nameElement = element.firstChildElement(QLatin1String("n")); + if (!nameElement.isNull()) { + Parameters parameters(nameElement.firstChildElement(QLatin1String("parameters"))); + names = { parameters, Names(nameElement) }; + } + + VCardHelper::fillContainer(element, "fn", fullName); + VCardHelper::fillContainer(element, "nickname", nickname); + VCardHelper::fillContainer(element, "org", org); + VCardHelper::fillContainer(element, "categories", categories); + VCardHelper::fillContainer(element, "title", title); + VCardHelper::fillContainer(element, "role", role); + VCardHelper::fillContainer(element, "note", note); + VCardHelper::fillContainer(element, "fburl", busyTimeUrl); + VCardHelper::fillContainer(element, "caladruri", calendarRequestUri); + VCardHelper::fillContainer(element, "url", urls); + VCardHelper::fillContainer(element, "caluri", calendarUri); + VCardHelper::fillContainer(element, "impp", impp); + VCardHelper::fillContainer(element, "geo", geo); + VCardHelper::fillContainer(element, "tel", tels); + VCardHelper::fillContainer(element, "email", emails); + VCardHelper::fillContainer(element, "key", key); + + VCardHelper::unserialize(element, "bday", bday); + VCardHelper::unserialize(element, "anniversary", anniversary); + + QDomElement genderElement = element.firstChildElement(QLatin1String("sex")); + if (!genderElement.isNull()) { + gender = VCardHelper::stringToGender(genderElement.text()); + } + + QDomElement genderCommentElement = genderElement.nextSiblingElement(QLatin1String("identity")); + if (!genderCommentElement.isNull()) { + genderComment = VCardHelper::extractText(genderCommentElement, "text"); + } + + uid = VCardHelper::extractText(element, "uid"); + kind = VCardHelper::extractText(element, "kind"); + + foreachElement(element, QLatin1String("clientpidmap"), [this](QDomElement e) { + int sourceId = VCardHelper::extractText(e, "sourceid").toInt(); + QString uri = VCardHelper::extractText(e, "uri"); + clientPidMap.insert(sourceId, uri); + }); + + foreachElement(element, QLatin1String("lang"), [this](QDomElement e) { + Parameters parameters(e.firstChildElement(QLatin1String("parameters"))); + QString langValue = VCardHelper::extractText(e, "language-tag"); + lang.append({ parameters, langValue }); + }); + + VCardHelper::fillContainer(element, "logo", logo); + VCardHelper::fillContainer(element, "member", member); + VCardHelper::fillContainer(element, "photo", photo); + VCardHelper::fillContainer(element, "sound", sound); + VCardHelper::fillContainer(element, "source", source); + VCardHelper::fillContainer(element, "tz", timeZone); + + prodid = VCardHelper::extractText(element, "prodid"); + + VCardHelper::fillContainer(element, "related", related); + + QDomElement revElement = element.firstChildElement(QLatin1String("rev")); + if (!revElement.isNull()) { + rev = QDateTime::fromString(VCardHelper::extractText(revElement, "timestamp"), Qt::ISODate); + } + + foreachElement(element, QLatin1String("adr"), [this](QDomElement e) { + Parameters parameters(e.firstChildElement(QLatin1String("parameters"))); + Address addressValue(e); + addresses.append({ parameters, addressValue }); + }); + } + + bool isEmpty() const + { + return fullName.isEmpty() && names.data.isEmpty() && nickname.isEmpty() && emails.isEmpty() && tels.isEmpty() + && org.isEmpty() && title.isEmpty() && role.isEmpty() && note.isEmpty() && urls.isEmpty() + && VCardHelper::isNull(bday) && VCardHelper::isNull(anniversary) && gender == VCard4::Gender::Undefined + && uid.isEmpty() && kind.isEmpty() && categories.isEmpty() && busyTimeUrl.isEmpty() + && calendarRequestUri.isEmpty() && calendarUri.isEmpty() && clientPidMap.isEmpty() && geo.isEmpty() + && impp.isEmpty() && key.isEmpty() && lang.isEmpty() && logo.isEmpty() && member.isEmpty() + && photo.isEmpty() && prodid.isEmpty() && related.isEmpty() && rev.isNull() && sound.isEmpty() + && source.isEmpty() && timeZone.isEmpty() && addresses.isEmpty(); + } +}; + +VCard::VCard() : d(nullptr) { } + +VCard::VCard(const VCard &other) : d(other.d) { } + +VCard &VCard::operator=(const VCard &other) +{ + d = other.d; + return *this; +} + +void VCard::detach() { d.detach(); } + +VCard::VCard(const QDomElement &element) : d(new VCardData(element)) { } + +VCard::~VCard() = default; + +bool VCard::isEmpty() const +{ + if (!d) + return true; + return d->isEmpty(); +} + +QDomElement VCard::toXmlElement(QDomDocument &document) const +{ + if (!d) { + return {}; + } + + QDomElement vCardElement = document.createElementNS(VCARD_NAMESPACE, QLatin1String("vcard")); + + VCardHelper::serializeList(vCardElement, d->fullName, QLatin1String("fn")); + if (!d->names.data.isEmpty()) { + auto e = vCardElement.appendChild(d->names.data.toXmlElement(document)).toElement(); + d->names.parameters.addTo(e); + } + VCardHelper::serializeList(vCardElement, d->nickname, QLatin1String("nickname"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->emails, QLatin1String("email"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->tels, QLatin1String("tel"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->org, QLatin1String("org"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->title, QLatin1String("title"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->role, QLatin1String("role"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->note, QLatin1String("note"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->urls, QLatin1String("url"), QLatin1String("uri")); + + VCardHelper::serialize(vCardElement, d->bday, "bday"); + VCardHelper::serialize(vCardElement, d->anniversary, "anniversary"); + + if (d->gender != VCard4::Gender::Undefined) { + QDomElement genderElement = document.createElement(QLatin1String("gender")); + genderElement.appendChild(document.createTextNode(VCardHelper::genderToString(d->gender))); + if (!d->genderComment.isEmpty()) { + VCardHelper::addTextElement(document, genderElement, QLatin1String("identity"), d->genderComment); + } + vCardElement.appendChild(genderElement); + } + + VCardHelper::serializeList(vCardElement, d->categories, QLatin1String("categories"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->busyTimeUrl, QLatin1String("fburl"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->calendarRequestUri, QLatin1String("caladruri"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->calendarUri, QLatin1String("caluri"), QLatin1String("uri")); + for (auto it = d->clientPidMap.cbegin(); it != d->clientPidMap.cend(); ++it) { + auto m = vCardElement.appendChild(document.createElement(QLatin1String("clientpidmap"))); + m.appendChild(document.createElement(QLatin1String("sourceid"))) + .appendChild(document.createTextNode(QString::number(it.key()))); + m.appendChild(document.createElement(QLatin1String("uri"))).appendChild(document.createTextNode(it.value())); + } + VCardHelper::serializeList(vCardElement, d->geo, QLatin1String("geo"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->impp, QLatin1String("impp"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->key, QLatin1String("key"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->lang, QLatin1String("lang"), QLatin1String("language-tag")); + VCardHelper::serializeList(vCardElement, d->logo, QLatin1String("logo"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->member, QLatin1String("member"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->photo, QLatin1String("photo"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->related, QLatin1String("related"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->timeZone, QLatin1String("tz"), QLatin1String("text")); + VCardHelper::serializeList(vCardElement, d->sound, QLatin1String("sound"), QLatin1String("uri")); + VCardHelper::serializeList(vCardElement, d->source, QLatin1String("source"), QLatin1String("uri")); + + if (d->rev.isValid()) { + auto revE = vCardElement.appendChild(document.createElement(QLatin1String("rev"))) + .appendChild(document.createTextNode(d->rev.toString(Qt::ISODate))); + } + + for (const auto &address : d->addresses) { + auto addrEl = address.data.toXmlElement(document); + if (!addrEl.isNull()) { + address.parameters.addTo(addrEl); + vCardElement.appendChild(addrEl); + } + } + + return vCardElement; +} + +VCard VCard::fromFile(const QString &filename) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return VCard(); + + auto vcard = fromDevice(&file); + file.close(); + return vcard; +} + +VCard VCard::fromDevice(QIODevice *dev) +{ + QDomDocument doc; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if (!doc.setContent(dev, true)) { +#else + if (!doc.setContent(dev, QDomDocument::ParseOption::UseNamespaceProcessing)) { +#endif + return {}; + } + + QDomElement root = doc.documentElement(); + if (root.tagName() != QLatin1String("vcards") || root.namespaceURI() != VCARD_NAMESPACE) + return {}; + + QDomElement vCardElement = root.firstChildElement(QLatin1String("vcard")); + if (vCardElement.isNull()) + return {}; + + return VCard(vCardElement); +} + +bool VCard::save(const QString &filename) const +{ + if (!d) + return false; + + QFile file(filename); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + return false; + + QDomDocument doc; + + QDomProcessingInstruction instr = doc.createProcessingInstruction("xml", "version='1.0' encoding='UTF-8'"); + doc.appendChild(instr); + + QDomElement root = doc.createElementNS(VCARD_NAMESPACE, QLatin1String("vcards")); + doc.appendChild(root); + + QDomElement vCardElement = toXmlElement(doc); + root.appendChild(vCardElement); + + QTextStream stream(&file); + doc.save(stream, 4); + file.close(); + + return true; +} + +void VCard::fromVCardTemp(const XMPP::VCard &tempVCard) +{ + // Helper function to convert boolean flags to parameters + auto convertToParameters = [](const XMPP::VCard::Address &address) { + Parameters params; + if (address.home) + params.type.append("home"); + if (address.work) + params.type.append("work"); + if (address.postal) + params.type.append("postal"); + if (address.parcel) + params.type.append("parcel"); + if (address.dom) + params.type.append("dom"); + if (address.intl) + params.type.append("intl"); + if (address.pref) + params.pref = 1; // Pref is true, setting as preferred + return params; + }; + + // Full Name + if (!tempVCard.fullName().isEmpty()) { + setFullName({ { PString { Parameters(), tempVCard.fullName() } } }); + } + + // Names + PNames names; + names.parameters = Parameters(); + if (!tempVCard.familyName().isEmpty()) + names.data.surname << tempVCard.familyName(); + if (!tempVCard.givenName().isEmpty()) + names.data.given << tempVCard.givenName(); + if (!tempVCard.middleName().isEmpty()) + names.data.additional << tempVCard.middleName(); + if (!tempVCard.prefixName().isEmpty()) + names.data.prefix << tempVCard.prefixName(); + if (!tempVCard.suffixName().isEmpty()) + names.data.suffix << tempVCard.suffixName(); + setNames(names); + + // Nickname + if (!tempVCard.nickName().isEmpty()) { + setNickName({ tempVCard.nickName() }); + } + + // Photo + if (!tempVCard.photo().isEmpty()) { + setPhoto(UriValue(tempVCard.photo(), QLatin1String("image/jpeg"))); + } else if (!tempVCard.photoURI().isEmpty()) { + setPhoto(UriValue(tempVCard.photoURI())); + } + + // Birthday + if (!tempVCard.bday().isNull()) { + setBday(tempVCard.bday()); + } else if (!tempVCard.bdayStr().isEmpty()) { + setBday(tempVCard.bdayStr()); + } + + // Addresses + PAddresses addresses; + for (const auto &addr : tempVCard.addressList()) { + PAddress pa { convertToParameters(addr), Address { addr } }; + if (!pa.data.isEmpty()) { + addresses.append(pa); + } + } + setAddresses(addresses); + + // Phones + PUrisOrTexts phones; + for (const auto &phone : tempVCard.phoneList()) { + Parameters params; + if (phone.home) + params.type.append("home"); + if (phone.work) + params.type.append("work"); + if (phone.voice) + params.type.append("voice"); + if (phone.fax) + params.type.append("fax"); + if (phone.pager) + params.type.append("pager"); + if (phone.msg) + params.type.append("msg"); + if (phone.cell) + params.type.append("cell"); + if (phone.video) + params.type.append("video"); + if (phone.bbs) + params.type.append("bbs"); + if (phone.modem) + params.type.append("modem"); + if (phone.isdn) + params.type.append("isdn"); + if (phone.pcs) + params.type.append("pcs"); + if (phone.pref) + params.pref = 1; // Pref is true, setting as preferred + if (!phone.number.isEmpty()) { + phones.append({ params, phone.number }); + } + } + if (!phones.isEmpty()) { + setPhones(phones); + } + + // Emails + PStrings emails; + for (const auto &email : tempVCard.emailList()) { + Parameters params; + if (email.home) + params.type.append("home"); + if (email.work) + params.type.append("work"); + if (email.internet) + params.type.append("internet"); + if (email.x400) + params.type.append("x400"); + if (email.pref) + params.pref = 1; // Pref is true, setting as preferred + if (!email.userid.isEmpty()) { + emails.append({ params, email.userid }); + } + } + if (!emails.isEmpty()) { + setEmails(emails); + } + + // JID + if (!tempVCard.jid().isEmpty()) { + setImpp(tempVCard.jid()); + } + + // Title + if (!tempVCard.title().isEmpty()) { + setTitle(tempVCard.title()); + } + + // Role + if (!tempVCard.role().isEmpty()) { + setRole(tempVCard.role()); + } + + // Logo + if (!tempVCard.logo().isEmpty()) { + setLogo(UriValue(tempVCard.logo(), QLatin1String("image/jpeg;base64"))); + } else if (!tempVCard.logoURI().isEmpty()) { + setLogo(UriValue(tempVCard.logoURI())); + } + + // Org + PStringLists org; + if (!tempVCard.org().name.isEmpty()) { + org.append({ Parameters(), { tempVCard.org().name } }); + for (const auto &unit : tempVCard.org().unit) { + if (!unit.isEmpty()) { + org.append({ Parameters(), { unit } }); + } + } + setOrg(org); + } + + // Categories + if (!tempVCard.categories().isEmpty()) { + setCategories(tempVCard.categories()); + } + + // Note + if (!tempVCard.note().isEmpty()) { + setNote(tempVCard.note()); + } + + // ProdId + if (!tempVCard.prodId().isEmpty()) { + setProdid(tempVCard.prodId()); + } + + // Rev + if (!tempVCard.rev().isEmpty()) { + setRev(QDateTime::fromString(tempVCard.rev(), Qt::ISODate)); + } + + // UID + if (!tempVCard.uid().isEmpty()) { + setUid(tempVCard.uid()); + } + + // URL + if (!tempVCard.url().isEmpty()) { + setUrls(QUrl(tempVCard.url())); + } + + // Geo + if (!tempVCard.geo().lat.isEmpty() && !tempVCard.geo().lon.isEmpty()) { + setGeo(QUrl("geo:" + tempVCard.geo().lat + "," + tempVCard.geo().lon)); + } + + // Timezone + if (!tempVCard.timezone().isEmpty()) { + setTimeZone(tempVCard.timezone()); + } + + // Desc (translated to note) + if (!tempVCard.desc().isEmpty()) { + setNote(tempVCard.desc()); + } + + // Sound + if (!tempVCard.sound().isEmpty()) { + setSound(UriValue(tempVCard.sound(), QLatin1String("audio/wav"))); + } else if (!tempVCard.soundURI().isEmpty()) { + setSound(UriValue(tempVCard.soundURI())); + } +} + +XMPP::VCard VCard::toVCardTemp() const +{ + auto tempVCard = XMPP::VCard::makeEmpty(); + + // Full Name + if (!d->fullName.isEmpty()) { + tempVCard.setFullName(d->fullName.preferred().data); + } + + // Names + tempVCard.setGivenName(d->names.data.given.value(0)); + tempVCard.setMiddleName(d->names.data.additional.value(0)); + tempVCard.setFamilyName(d->names.data.surname.value(0)); + + // Nickname + if (!d->nickname.isEmpty()) { + tempVCard.setNickName(d->nickname.preferred().data.value(0)); + } + + // Birthday + if (!VCardHelper::isNull(d->bday)) { + QDate bday = d->bday; + if (bday.isValid()) { + tempVCard.setBday(bday); + } else { + tempVCard.setBdayStr(d->bday); + } + } + + // Email + if (!d->emails.isEmpty()) { + XMPP::VCard::Email email; + const auto &preferredEmail = d->emails.preferred(); + email.userid = preferredEmail.data; + if (preferredEmail.parameters.pref > 0) { + email.pref = true; + } + for (const auto &type : preferredEmail.parameters.type) { + if (type == "home") { + email.home = true; + } else if (type == "work") { + email.work = true; + } else if (type == "internet") { + email.internet = true; + } else if (type == "x400") { + email.x400 = true; + } + } + XMPP::VCard::EmailList emailList; + emailList << email; + tempVCard.setEmailList(emailList); + } + + // URL + if (!d->urls.isEmpty()) { + tempVCard.setUrl(d->urls.preferred().data.toString()); + } + + // Phone + if (!d->tels.isEmpty()) { + XMPP::VCard::Phone phone; + const auto &preferredPhone = d->tels.preferred(); + phone.number = std::get(preferredPhone.data); + for (const auto &type : preferredPhone.parameters.type) { + if (type == "home") { + phone.home = true; + } else if (type == "voice") { + phone.voice = true; + } + } + XMPP::VCard::PhoneList phoneList; + phoneList << phone; + tempVCard.setPhoneList(phoneList); + } + + // Photo + tempVCard.setPhoto(d->photo); + + // Address + if (!d->addresses.isEmpty()) { + XMPP::VCard::AddressList addressList; + for (const auto &address : d->addresses) { + XMPP::VCard::Address addr; + addr.home = address.parameters.type.contains("home"); + addr.street = address.data.pobox.value(0); + addr.extaddr = address.data.extaddr.value(0); + addr.locality = address.data.street.value(0); + addr.region = address.data.locality.value(0); + addr.pcode = address.data.region.value(0); + addr.country = address.data.country.value(0); + addressList << addr; + } + tempVCard.setAddressList(addressList); + } + + // Organization + if (!d->org.isEmpty()) { + XMPP::VCard::Org org; + org.name = d->org.value(0).data.value(0); + for (int i = 1; i < d->org.size(); ++i) { + org.unit << d->org[i].data.value(0); + } + tempVCard.setOrg(org); + } + + tempVCard.setTitle(d->title.preferred().data); + tempVCard.setRole(d->role.preferred().data); + tempVCard.setNote(d->note.preferred().data); + + return tempVCard; +} + +// Getters and setters implementation + +PStrings VCard::fullName() const { return d ? d->fullName : PStrings {}; } + +void VCard::setFullName(const PStrings &fullName) +{ + INIT_D(); + d->fullName = fullName; +} + +Item &VCard::setFullName(const QString &fullName) +{ + INIT_D(); + d->fullName.append({ Parameters(), fullName }); + return d->fullName.last(); +} + +PNames VCard::names() const { return d ? d->names : PNames {}; } + +void VCard::setNames(const PNames &names) +{ + INIT_D(); + d->names = names; +} + +Item &VCard::setNames(const Names &names) +{ + INIT_D(); + d->names = { Parameters(), names }; + return d->names; +} + +PStringLists VCard::nickName() const { return d ? d->nickname : PStringLists(); } + +void VCard::setNickName(const PStringLists &nickname) +{ + INIT_D(); + d->nickname = nickname; +} + +Item &VCard::setNickName(const QStringList &nickname) +{ + INIT_D(); + d->nickname.append({ Parameters(), nickname }); + return d->nickname.last(); +} + +PStrings VCard::emails() const { return d ? d->emails : PStrings(); } + +void VCard::setEmails(const PStrings &emails) +{ + INIT_D(); + d->emails = emails; +} + +Item &VCard::setEmails(const QString &email) +{ + INIT_D(); + d->emails.append({ Parameters(), email }); + return d->emails.last(); +} + +PUrisOrTexts VCard::phones() const { return d ? d->tels : PUrisOrTexts(); } + +void VCard::setPhones(const PUrisOrTexts &tels) +{ + INIT_D(); + d->tels = tels; +} + +Item &VCard::setPhones(const UriOrText &phone) +{ + INIT_D(); + d->tels.append({ Parameters(), phone }); + return d->tels.last(); +} + +PStringLists VCard::org() const { return d ? d->org : PStringLists(); } + +void VCard::setOrg(const PStringLists &org) +{ + INIT_D(); + d->org = org; +} + +Item &VCard::setOrg(const QStringList &org) +{ + INIT_D(); + d->org.append({ Parameters(), org }); + return d->org.last(); +} + +PStrings VCard::title() const { return d ? d->title : PStrings(); } + +void VCard::setTitle(const PStrings &title) +{ + INIT_D(); + d->title = title; +} + +Item &VCard::setTitle(const QString &title) +{ + INIT_D(); + d->title.append({ Parameters(), title }); + return d->title.last(); +} + +PStrings VCard::role() const { return d ? d->role : PStrings(); } + +void VCard::setRole(const PStrings &role) +{ + INIT_D(); + d->role = role; +} + +Item &VCard::setRole(const QString &role) +{ + INIT_D(); + d->role.append({ Parameters(), role }); + return d->role.last(); +} + +PStrings VCard::note() const { return d ? d->note : PStrings(); } + +void VCard::setNote(const PStrings ¬e) +{ + INIT_D(); + d->note = note; +} + +Item &VCard::setNote(const QString ¬e) +{ + INIT_D(); + d->note.append({ Parameters(), note }); + return d->note.last(); +} + +PUris VCard::urls() const { return d ? d->urls : PUris(); } + +void VCard::setUrls(const PUris &urls) +{ + INIT_D(); + d->urls = urls; +} + +Item &VCard::setUrls(const QUrl &url) +{ + INIT_D(); + d->urls.append({ Parameters(), url }); + return d->urls.last(); +} + +PHistorical VCard::bday() const { return d ? d->bday : PHistorical(); } + +void VCard::setBday(const PHistorical &bday) +{ + INIT_D(); + d->bday = bday; +} + +Item &VCard::setBday(const Historical &bday) +{ + INIT_D(); + d->bday = { Parameters(), bday }; + return d->bday; +} + +PHistorical VCard::anniversary() const { return d ? d->anniversary : PHistorical(); } + +void VCard::setAnniversary(const PHistorical &anniversary) +{ + INIT_D(); + d->anniversary = anniversary; +} + +Item &VCard::setAnniversary(const Historical &anniversary) +{ + INIT_D(); + d->anniversary = { Parameters(), anniversary }; + return d->anniversary; +} + +VCard4::Gender VCard::gender() const { return d ? d->gender : VCard4::Gender::Undefined; } + +void VCard::setGender(Gender gender) +{ + INIT_D(); + d->gender = gender; +} + +QString VCard::genderComment() const { return d ? d->genderComment : QString(); } + +void VCard::setGenderComment(const QString &comment) +{ + INIT_D(); + d->genderComment = comment; +} + +QString VCard::uid() const { return d ? d->uid : QString(); } + +void VCard::setUid(const QString &uid) +{ + INIT_D(); + d->uid = uid; +} + +QString VCard::kind() const { return d ? d->kind : QString(); } + +void VCard::setKind(const QString &kind) +{ + INIT_D(); + d->kind = kind; +} + +PStringLists VCard::categories() const { return d ? d->categories : PStringLists(); } + +void VCard::setCategories(const PStringLists &categories) +{ + INIT_D(); + d->categories = categories; +} + +Item &VCard::setCategories(const QStringList &categories) +{ + INIT_D(); + d->categories.append({ Parameters(), categories }); + return d->categories.last(); +} + +PUris VCard::busyTimeUrl() const { return d ? d->busyTimeUrl : PUris(); } + +void VCard::setBusyTimeUrl(const PUris &busyTimeUrl) +{ + INIT_D(); + d->busyTimeUrl = busyTimeUrl; +} + +Item &VCard::setBusyTimeUrl(const QUrl &url) +{ + INIT_D(); + d->busyTimeUrl.append({ Parameters(), url }); + return d->busyTimeUrl.last(); +} + +PUris VCard::calendarRequestUri() const { return d ? d->calendarRequestUri : PUris(); } + +void VCard::setCalendarRequestUri(const PUris &calendarRequestUri) +{ + INIT_D(); + d->calendarRequestUri = calendarRequestUri; +} + +Item &VCard::setCalendarRequestUri(const QUrl &url) +{ + INIT_D(); + d->calendarRequestUri.append({ Parameters(), url }); + return d->calendarRequestUri.last(); +} + +PUris VCard::calendarUri() const { return d ? d->calendarUri : PUris(); } + +void VCard::setCalendarUri(const PUris &calendarUri) +{ + INIT_D(); + d->calendarUri = calendarUri; +} + +Item &VCard::setCalendarUri(const QUrl &url) +{ + INIT_D(); + d->calendarUri.append({ Parameters(), url }); + return d->calendarUri.last(); +} + +QHash VCard::clientPidMap() const { return d ? d->clientPidMap : QHash(); } + +void VCard::setClientPidMap(const QHash &clientPidMap) +{ + INIT_D(); + d->clientPidMap = clientPidMap; +} + +PUris VCard::geo() const { return d ? d->geo : PUris(); } + +void VCard::setGeo(const PUris &geo) +{ + INIT_D(); + d->geo = geo; +} + +Item &VCard::setGeo(const QUrl &url) +{ + INIT_D(); + d->geo.append({ Parameters(), url }); + return d->geo.last(); +} + +PUris VCard::impp() const { return d ? d->impp : PUris(); } + +void VCard::setImpp(const PUris &impp) +{ + INIT_D(); + d->impp = impp; +} + +Item &VCard::setImpp(const QUrl &url) +{ + INIT_D(); + d->impp.append({ Parameters(), url }); + return d->impp.last(); +} + +PUrisOrTexts VCard::key() const { return d ? d->key : PUrisOrTexts(); } + +void VCard::setKey(const PUrisOrTexts &key) +{ + INIT_D(); + d->key = key; +} + +Item &VCard::setKey(const UriOrText &key) +{ + INIT_D(); + d->key.append({ Parameters(), key }); + return d->key.last(); +} + +PStrings VCard::languages() const { return d ? d->lang : PStrings(); } + +void VCard::setLanguages(const PStrings &lang) +{ + INIT_D(); + d->lang = lang; +} + +Item &VCard::setLanguages(const QString &lang) +{ + INIT_D(); + d->lang.append({ Parameters(), lang }); + return d->lang.last(); +} + +PAdvUris VCard::logo() const { return d ? d->logo : PAdvUris(); } + +void VCard::setLogo(const PAdvUris &logo) +{ + INIT_D(); + d->logo = logo; +} + +Item &VCard::setLogo(const UriValue &logo) +{ + INIT_D(); + d->logo.append({ Parameters(), logo }); + return d->logo.last(); +} + +PUris VCard::member() const { return d ? d->member : PUris(); } + +void VCard::setMember(const PUris &member) +{ + INIT_D(); + d->member = member; +} + +Item &VCard::setMember(const QUrl &member) +{ + INIT_D(); + d->member.append({ Parameters(), member }); + return d->member.last(); +} + +PAdvUris VCard::photo() const { return d ? d->photo : PAdvUris(); } + +void VCard::setPhoto(const PAdvUris &photo) +{ + INIT_D(); + d->photo = photo; +} + +Item &VCard::setPhoto(const UriValue &photo) +{ + INIT_D(); + d->photo.append({ Parameters(), photo }); + return d->photo.last(); +} + +QString VCard::prodid() const { return d ? d->prodid : QString(); } + +void VCard::setProdid(const QString &prodid) +{ + INIT_D(); + d->prodid = prodid; +} + +PUrisOrTexts VCard::related() const { return d ? d->related : PUrisOrTexts(); } + +void VCard::setRelated(const PUrisOrTexts &related) +{ + INIT_D(); + d->related = related; +} + +Item &VCard::setRelated(const UriOrText &related) +{ + INIT_D(); + d->related.append({ Parameters(), related }); + return d->related.last(); +} + +QDateTime VCard::rev() const { return d ? d->rev : QDateTime(); } + +void VCard::setRev(const QDateTime &rev) +{ + INIT_D(); + d->rev = rev; +} + +PAdvUris VCard::sound() const { return d ? d->sound : PAdvUris(); } + +void VCard::setSound(const PAdvUris &sound) +{ + INIT_D(); + d->sound = sound; +} + +Item &VCard::setSound(const UriValue &sound) +{ + INIT_D(); + d->sound.append({ Parameters(), sound }); + return d->sound.last(); +} + +PUris VCard::source() const { return d ? d->source : PUris(); } + +void VCard::setSource(const PUris &source) +{ + INIT_D(); + d->source = source; +} + +Item &VCard::setSource(const QUrl &source) +{ + INIT_D(); + d->source.append({ Parameters(), source }); + return d->source.last(); +} + +PTimeZones VCard::timeZone() const { return d ? d->timeZone : PTimeZones(); } + +void VCard::setTimeZone(const PTimeZones &timeZone) +{ + INIT_D(); + d->timeZone = timeZone; +} + +Item &VCard::setTimeZone(const TimeZone &timeZone) +{ + INIT_D(); + d->timeZone.append({ Parameters(), timeZone }); + return d->timeZone.last(); +} + +PAddresses VCard::addresses() const { return d ? d->addresses : PAddresses(); } + +void VCard::setAddresses(const PAddresses &addresses) +{ + INIT_D(); + d->addresses = addresses; +} + +Item
&VCard::setAddresses(const Address &addresses) +{ + INIT_D(); + d->addresses.append({ Parameters(), addresses }); + return d->addresses.last(); +} + +} // namespace VCard4 diff --git a/src/xmpp/xmpp-im/xmpp_vcard4.h b/src/xmpp/xmpp-im/xmpp_vcard4.h new file mode 100644 index 00000000..66166757 --- /dev/null +++ b/src/xmpp/xmpp-im/xmpp_vcard4.h @@ -0,0 +1,431 @@ +/* + * xmpp_vcard4.cpp - classes for handling vCards according to rfc6351 + * Copyright (C) 2024 Sergei Ilinykh + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + * + */ + +#ifndef XMPP_VCARD4_H +#define XMPP_VCARD4_H + +#include "xmpp_vcard.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class QFile; + +/** + * This code represents implementation of RFC 6351/6350 as well as XEP-0292 + */ + +namespace XMPP::VCard4 { + +enum class Gender { Undefined, Male, Female, Other, None, Unknown }; + +class Parameters { +public: + Parameters() = default; + Parameters(const QDomElement &element); + void addTo(QDomElement parent) const; + bool isEmpty() const; + + QStringList type; + QString language; + QString altid; + QString pid; + int pref = 0; // Preference (1 to 100) + QString geo; + QString tz; // Time zone + QString label; +}; + +class Names { +public: + Names() = default; + Names(const QDomElement &element); + QDomElement toXmlElement(QDomDocument &document) const; + bool isEmpty() const noexcept; + + QStringList surname; + QStringList given; + QStringList additional; + QStringList prefix; + QStringList suffix; +}; + +class Address { +public: + Address() = default; + Address(const QDomElement &element); + Address(const XMPP::VCard::Address &legacyAddress) + { + if (!legacyAddress.pobox.isEmpty()) { + pobox.append(legacyAddress.pobox); + } + if (!legacyAddress.extaddr.isEmpty()) { + extaddr.append(legacyAddress.extaddr); + } + if (!legacyAddress.street.isEmpty()) { + street.append(legacyAddress.street); + } + if (!legacyAddress.locality.isEmpty()) { + locality.append(legacyAddress.locality); + } + if (!legacyAddress.region.isEmpty()) { + region.append(legacyAddress.region); + } + if (!legacyAddress.pcode.isEmpty()) { + code.append(legacyAddress.pcode); + } + if (!legacyAddress.country.isEmpty()) { + country.append(legacyAddress.country); + } + } + QDomElement toXmlElement(QDomDocument &document) const; + bool isEmpty() const noexcept; + + QStringList pobox; + QStringList extaddr; + QStringList street; + QStringList locality; + QStringList region; + QStringList code; + QStringList country; +}; + +class UriValue { +public: + UriValue() = default; + explicit UriValue(const QString &uri); + explicit UriValue(const QByteArray &data, const QString &mime); + QString toString() const; + inline operator QString() const { return toString(); } + bool isEmpty() const { return url.isEmpty() && data.isEmpty(); } + + QUrl url; + QByteArray data; + QString mediaType; +}; + +using UriOrText = std::variant; +using TimeZone = std::variant; +using Historical = std::variant; + +template struct ItemBase { + Parameters parameters; + T data; +}; + +template struct Item : public ItemBase { + operator QString() const { return this->data; } // maybe convertible by Qt means + operator QDate() const { return QDate(this->data); } + operator QUrl() const { return QUrl(this->data); } +}; + +template <> struct Item : public ItemBase { + operator QString() const { return data.toString(Qt::ISODate); } + operator QDate() const { return data; } +}; + +template <> struct Item : public ItemBase { + operator QString() const { return data.toString(Qt::ISODate); } + operator QDate() const { return data.date(); } +}; + +template <> struct Item : public ItemBase { + operator QString() const { return data.value(0); } +}; + +template <> struct Item : public ItemBase { + operator QString() const + { + return std::visit( + [](auto const &v) { + using Tv = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } else { + return v.toString(Qt::ISODate); + } + }, + data); + } + operator QDate() const + { + return std::visit( + [](auto const &v) { + using Tv = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } + if constexpr (std::is_same_v) { + return v.date(); + } else { + return QDate {}; + } + }, + data); + } +}; + +template <> struct Item : public ItemBase { + operator QString() const + { + return std::visit( + [](auto const &v) { + using Tv = std::decay_t; + if constexpr (std::is_same_v) { + return v; + } else { + return v.toString(); + } + }, + data); + } +}; + +using PStringList = Item; +using PString = Item; +using PUri = Item; +using PDate = Item; +using PAdvUri = Item; +using PAddress = Item
; +using PNames = Item; +using PUriOrText = Item; +using PTimeZone = Item; +using PHistorical = Item; + +template class TaggedList : public QList { +public: + using item_type = T; + + T preferred() const + { + if (this->empty()) { + return {}; + } + return *std::ranges::max_element( + *this, [](auto const &a, auto const &b) { return a.parameters.pref > b.parameters.pref; }); + } + + operator QString() const { return preferred(); } + operator QUrl() const { return preferred(); } +}; + +template <> class TaggedList : public QList { +public: + using item_type = PAdvUri; + + operator QByteArray() const + { + // take first preferred data uri and its data + if (this->empty()) { + return {}; + } + return std::ranges::max_element(*this, + [](auto const &a, auto const &b) { + return ((int(!a.data.data.isEmpty()) << 8) + a.parameters.pref) + > ((int(!b.data.data.isEmpty()) << 8) + b.parameters.pref); + }) + ->data.data; + } +}; + +using PStringLists = TaggedList; +using PStrings = TaggedList; +using PUris = TaggedList; +using PAdvUris = TaggedList; +using PAddresses = TaggedList; +using PUrisOrTexts = TaggedList; +using PTimeZones = TaggedList; + +class VCard { +public: + VCard(); + VCard(const QDomElement &element); + VCard(const VCard &other); + + ~VCard(); + + VCard &operator=(const VCard &); + void detach(); + + bool isEmpty() const; + + inline bool isNull() const { return d != nullptr; } + inline explicit operator bool() const { return isNull(); } + + QDomElement toXmlElement(QDomDocument &document) const; + + static VCard fromFile(const QString &filename); + static VCard fromDevice(QIODevice *dev); + bool save(const QString &filename) const; + + void fromVCardTemp(const XMPP::VCard &tempVCard); + XMPP::VCard toVCardTemp() const; + + // Getters and setters + PStrings fullName() const; + void setFullName(const PStrings &fullName); + Item &setFullName(const QString &fullName); + + PNames names() const; + void setNames(const PNames &names); + Item &setNames(const Names &names); + + PStringLists nickName() const; + void setNickName(const PStringLists &nickname); + Item &setNickName(const QStringList &nickname); + + PStrings emails() const; + void setEmails(const PStrings &emails); + Item &setEmails(const QString &email); + + PUrisOrTexts phones() const; + void setPhones(const PUrisOrTexts &tels); + Item &setPhones(const UriOrText &phone); + + PStringLists org() const; + void setOrg(const PStringLists &org); + Item &setOrg(const QStringList &org); + + PStrings title() const; + void setTitle(const PStrings &title); + Item &setTitle(const QString &title); + + PStrings role() const; + void setRole(const PStrings &role); + Item &setRole(const QString &role); + + PStrings note() const; + void setNote(const PStrings ¬e); + Item &setNote(const QString ¬e); + + PUris urls() const; + void setUrls(const PUris &urls); + Item &setUrls(const QUrl &url); + + PHistorical bday() const; + void setBday(const PHistorical &bday); + Item &setBday(const Historical &bday); + + PHistorical anniversary() const; + void setAnniversary(const PHistorical &anniversary); + Item &setAnniversary(const Historical &anniversary); + + Gender gender() const; + void setGender(Gender gender); + + QString genderComment() const; + void setGenderComment(const QString &comment); + + QString uid() const; + void setUid(const QString &uid); + + QString kind() const; + void setKind(const QString &kind); + + PStringLists categories() const; + void setCategories(const PStringLists &categories); + Item &setCategories(const QStringList &categories); + + PUris busyTimeUrl() const; + void setBusyTimeUrl(const PUris &busyTimeUrl); + Item &setBusyTimeUrl(const QUrl &url); + + PUris calendarRequestUri() const; + void setCalendarRequestUri(const PUris &calendarRequestUri); + Item &setCalendarRequestUri(const QUrl &url); + + PUris calendarUri() const; + void setCalendarUri(const PUris &calendarUri); + Item &setCalendarUri(const QUrl &url); + + QHash clientPidMap() const; + void setClientPidMap(const QHash &clientPidMap); + + PUris geo() const; + void setGeo(const PUris &geo); + Item &setGeo(const QUrl &url); + + PUris impp() const; + void setImpp(const PUris &impp); + Item &setImpp(const QUrl &url); + + PUrisOrTexts key() const; + void setKey(const PUrisOrTexts &key); + Item &setKey(const UriOrText &key); + + PStrings languages() const; + void setLanguages(const PStrings &lang); + Item &setLanguages(const QString &lang); + + PAdvUris logo() const; + void setLogo(const PAdvUris &logo); + Item &setLogo(const UriValue &logo); + + PUris member() const; + void setMember(const PUris &member); + Item &setMember(const QUrl &member); + + PAdvUris photo() const; + void setPhoto(const PAdvUris &photo); + Item &setPhoto(const UriValue &photo); + + QString prodid() const; + void setProdid(const QString &prodid); + + PUrisOrTexts related() const; + void setRelated(const PUrisOrTexts &related); + Item &setRelated(const UriOrText &related); + + QDateTime rev() const; + void setRev(const QDateTime &rev); + + PAdvUris sound() const; + void setSound(const PAdvUris &sound); + Item &setSound(const UriValue &sound); + + PUris source() const; + void setSource(const PUris &source); + Item &setSource(const QUrl &source); + + PTimeZones timeZone() const; + void setTimeZone(const PTimeZones &timeZone); + Item &setTimeZone(const TimeZone &timeZone); + + PAddresses addresses() const; + void setAddresses(const PAddresses &addresses); + Item
&setAddresses(const Address &addresses); + +private: + class VCardData; + QExplicitlySharedDataPointer d; +}; + +} // namespace VCard4 + +#endif // XMPP_VCARD4_H diff --git a/src/xmpp/xmpp-im/xmpp_xdata.cpp b/src/xmpp/xmpp-im/xmpp_xdata.cpp index 219d0f48..8af2def0 100644 --- a/src/xmpp/xmpp-im/xmpp_xdata.cpp +++ b/src/xmpp/xmpp-im/xmpp_xdata.cpp @@ -305,9 +305,7 @@ bool XData::Field::MediaElement::checkSupport(const QStringList &wildcards) #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if (QRegExp(wildcard, Qt::CaseSensitive, QRegExp::Wildcard).exactMatch(uri.mimeType)) { #else - if (QRegularExpression::fromWildcard(QLatin1Char('^') + wildcard + QLatin1Char('$'), Qt::CaseSensitive) - .match(uri.mimeType) - .hasMatch()) { + if (QRegularExpression::fromWildcard(wildcard, Qt::CaseSensitive).match(uri.mimeType).hasMatch()) { #endif return true; } @@ -336,6 +334,8 @@ void XData::setType(Type t) { d->type = t; } QString XData::registrarType() const { return d->registrarType; } +void XData::setRegistrarType(const QString ®istrarType) { d->registrarType = registrarType; } + const XData::FieldList &XData::fields() const { return d->fields; } XData::Field XData::getField(const QString &var) const @@ -455,6 +455,13 @@ QDomElement XData::toXml(QDomDocument *doc, bool submitForm) const x.appendChild(textTag(doc, "title", d->title)); if (!submitForm && !d->instructions.isEmpty()) x.appendChild(textTag(doc, "instructions", d->instructions)); + if (!d->registrarType.isEmpty()) { + XData::Field f; + f.setType(Field::Field_Hidden); + f.setVar(QLatin1String("FORM_TYPE")); + f.setValue({ d->registrarType }); + x.appendChild(f.toXml(doc, submitForm)); + } if (!d->fields.isEmpty()) { for (const auto &f : std::as_const(d->fields)) { diff --git a/src/xmpp/xmpp-im/xmpp_xdata.h b/src/xmpp/xmpp-im/xmpp_xdata.h index 29bcab6a..2a46acce 100644 --- a/src/xmpp/xmpp-im/xmpp_xdata.h +++ b/src/xmpp/xmpp-im/xmpp_xdata.h @@ -47,6 +47,7 @@ class XData { Type type() const; void setType(Type); QString registrarType() const; + void setRegistrarType(const QString ®istrarType); struct ReportField { ReportField() { }