// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#define QXMPP_DEBUG_STUN

#include "QXmppStunServer.h"
#include "QXmppStun_p.h"
#include "QXmppTurnServer.h"
#include "QXmppUtils.h"

#include "Algorithms.h"
#include "Enums.h"
#include "StringLiterals.h"

#include <QCryptographicHash>
#include <QDataStream>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QPointer>
#include <QTimer>
#include <QUdpSocket>
#include <QVariant>

#undef QXMPP_DEBUG_STUN

using namespace QXmpp;
using namespace QXmpp::Private;

static const quint32 STUN_MAGIC = 0x2112A442;
static const quint16 STUN_HEADER = 20;
static constexpr auto STUN_ID_SIZE = 12;
static const quint8 STUN_IPV4 = 0x01;
static const quint8 STUN_IPV6 = 0x02;
static constexpr auto STUN_RTO_INTERVAL = 500;
static constexpr auto STUN_RTO_MAX = 7;

template<>
struct Enums::Data<QXmppIceConnection::GatheringState> {
    using enum QXmppIceConnection::GatheringState;
    static constexpr auto Values = makeValues<QXmppIceConnection::GatheringState>({
        { NewGatheringState, u"new" },
        { BusyGatheringState, u"gathering" },
        { CompleteGatheringState, u"complete" },
    });
};

static const char *pair_states[] = {
    "frozen",
    "waiting",
    "in-progress",
    "succeeded",
    "failed"
};

enum AttributeType {
    MappedAddress = 0x0001,       // RFC5389
    ChangeRequest = 0x0003,       // RFC5389
    SourceAddress = 0x0004,       // RFC5389
    ChangedAddress = 0x0005,      // RFC5389
    Username = 0x0006,            // RFC5389
    MessageIntegrity = 0x0008,    // RFC5389
    ErrorCode = 0x0009,           // RFC5389
    ChannelNumber = 0x000c,       // RFC5766 : TURN
    Lifetime = 0x000d,            // RFC5766 : TURN
    XorPeerAddress = 0x0012,      // RFC5766 : TURN
    DataAttr = 0x0013,            // RFC5766 : TURN
    Realm = 0x0014,               // RFC5389
    Nonce = 0x0015,               // RFC5389
    XorRelayedAddress = 0x0016,   // RFC5766 : TURN
    EvenPort = 0x0018,            // RFC5766 : TURN
    RequestedTransport = 0x0019,  // RFC5766 : TURN
    XorMappedAddress = 0x0020,    // RFC5389
    ReservationToken = 0x0022,    // RFC5766 : TURN
    Priority = 0x0024,            // RFC5245
    UseCandidate = 0x0025,        // RFC5245
    Software = 0x8022,            // RFC5389
    Fingerprint = 0x8028,         // RFC5389
    IceControlled = 0x8029,       // RFC5245
    IceControlling = 0x802a,      // RFC5245
    OtherAddress = 0x802c         // RFC5780
};

// FIXME : we need to set local preference to discriminate between
// multiple IP addresses
static quint32 candidatePriority(const QXmppJingleCandidate &candidate, int localPref = 65535)
{
    int typePref;
    switch (candidate.type()) {
    case QXmppJingleCandidate::HostType:
        typePref = 126;
        break;
    case QXmppJingleCandidate::PeerReflexiveType:
        typePref = 110;
        break;
    case QXmppJingleCandidate::ServerReflexiveType:
        typePref = 100;
        break;
    default:
        typePref = 0;
    }

    return (1 << 24) * typePref +
        (1 << 8) * localPref +
        (256 - candidate.component());
}

static QString computeFoundation(QXmppJingleCandidate::Type type, const QString &protocol, const QHostAddress &baseAddress)
{
    QCryptographicHash hash(QCryptographicHash::Md5);
    hash.addData((QString::number(type) + protocol + baseAddress.toString()).toUtf8());
    return QString::fromUtf8(hash.result().toHex());
}

static bool isIPv6LinkLocalAddress(const QHostAddress &addr)
{
    if (addr.protocol() != QAbstractSocket::IPv6Protocol) {
        return false;
    }
    Q_IPV6ADDR ipv6addr = addr.toIPv6Address();
    return (((ipv6addr[0] << 8) + ipv6addr[1]) & 0xffc0) == 0xfe80;
}

static bool isLoopbackAddress(const QHostAddress &addr)
{
    return (addr.toIPv4Address() & 0xff000000) == 0x7f000000;
}

// Returns true if the two addresses are compatible.
static bool isCompatibleAddress(const QHostAddress &a1, const QHostAddress &a2)
{
    return a1.protocol() == a2.protocol() &&
        isIPv6LinkLocalAddress(a1) == isIPv6LinkLocalAddress(a2);
}

static bool decodeAddress(QDataStream &stream, quint16 a_length, QHostAddress &address, quint16 &port, const QByteArray &xorId = QByteArray())
{
    if (a_length < 4) {
        return false;
    }
    quint8 reserved, protocol;
    quint16 rawPort;
    stream >> reserved;
    stream >> protocol;
    stream >> rawPort;
    if (xorId.isEmpty()) {
        port = rawPort;
    } else {
        port = rawPort ^ (STUN_MAGIC >> 16);
    }
    if (protocol == STUN_IPV4) {
        if (a_length != 8) {
            return false;
        }
        quint32 addr;
        stream >> addr;
        if (xorId.isEmpty()) {
            address = QHostAddress(addr);
        } else {
            address = QHostAddress(addr ^ STUN_MAGIC);
        }
    } else if (protocol == STUN_IPV6) {
        if (a_length != 20) {
            return false;
        }
        Q_IPV6ADDR addr;
        stream.readRawData((char *)&addr, sizeof(addr));
        if (!xorId.isEmpty()) {
            QByteArray xpad;
            QDataStream(&xpad, QIODevice::WriteOnly) << STUN_MAGIC;
            xpad += xorId;
            for (int i = 0; i < 16; i++) {
                addr[i] ^= xpad[i];
            }
        }
        address = QHostAddress(addr);
    } else {
        return false;
    }
    return true;
}

static void encodeAddress(QDataStream &stream, quint16 type, const QHostAddress &address, quint16 port, const QByteArray &xorId = QByteArray())
{
    const quint8 reserved = 0;
    if (address.protocol() == QAbstractSocket::IPv4Protocol) {
        stream << type;
        stream << quint16(8);
        stream << reserved;
        stream << quint8(STUN_IPV4);
        quint32 addr = address.toIPv4Address();
        if (!xorId.isEmpty()) {
            port ^= (STUN_MAGIC >> 16);
            addr ^= STUN_MAGIC;
        }
        stream << port;
        stream << addr;
    } else if (address.protocol() == QAbstractSocket::IPv6Protocol) {
        stream << type;
        stream << quint16(20);
        stream << reserved;
        stream << quint8(STUN_IPV6);
        Q_IPV6ADDR addr = address.toIPv6Address();
        if (!xorId.isEmpty()) {
            port ^= (STUN_MAGIC >> 16);
            QByteArray xpad;
            QDataStream(&xpad, QIODevice::WriteOnly) << STUN_MAGIC;
            xpad += xorId;
            for (int i = 0; i < 16; i++) {
                addr[i] ^= xpad[i];
            }
        }
        stream << port;
        stream.writeRawData((char *)&addr, sizeof(addr));
    } else {
        qWarning("Cannot write STUN attribute for unknown IP version");
    }
}

static void addAddress(QDataStream &stream, quint16 type, const QHostAddress &host, quint16 port, const QByteArray &xorId = QByteArray())
{
    if (port && !host.isNull() &&
        (host.protocol() == QAbstractSocket::IPv4Protocol ||
         host.protocol() == QAbstractSocket::IPv6Protocol)) {
        encodeAddress(stream, type, host, port, xorId);
    }
}

static void encodeString(QDataStream &stream, quint16 type, const QString &string)
{
    const QByteArray utf8string = string.toUtf8();
    stream << type;
    stream << quint16(utf8string.size());
    stream.writeRawData(utf8string.data(), utf8string.size());
    if (utf8string.size() % 4) {
        const QByteArray padding(4 - (utf8string.size() % 4), 0);
        stream.writeRawData(padding.data(), padding.size());
    }
}

static void setBodyLength(QByteArray &buffer, qint16 length)
{
    QDataStream stream(&buffer, QIODevice::WriteOnly);
    stream.device()->seek(2);
    stream << length;
}

/*! Constructs a new QXmppStunMessage. */
QXmppStunMessage::QXmppStunMessage()
    : m_cookie(STUN_MAGIC)
{
    m_id = QByteArray(STUN_ID_SIZE, 0);
}

quint32 QXmppStunMessage::cookie() const
{
    return m_cookie;
}

void QXmppStunMessage::setCookie(quint32 cookie)
{
    m_cookie = cookie;
}

QByteArray QXmppStunMessage::id() const
{
    return m_id;
}

void QXmppStunMessage::setId(const QByteArray &id)
{
    Q_ASSERT(id.size() == STUN_ID_SIZE);
    m_id = id;
}

quint16 QXmppStunMessage::messageClass() const
{
    return m_type & 0x0110;
}

quint16 QXmppStunMessage::messageMethod() const
{
    return m_type & 0x3eef;
}

quint16 QXmppStunMessage::type() const
{
    return m_type;
}

void QXmppStunMessage::setType(quint16 type)
{
    m_type = type;
}

/*!
    Returns the CHANGE-REQUEST attribute, indicating whether to change
    the IP and / or port from which the response is sent.
*/
quint32 QXmppStunMessage::changeRequest() const
{
    return m_changeRequest;
}

/*!
    Sets the CHANGE-REQUEST attribute via \a changeRequest, indicating whether
    to change the IP and / or port from which the response is sent.
*/
void QXmppStunMessage::setChangeRequest(quint32 changeRequest)
{
    m_changeRequest = changeRequest;
    m_attributes << ChangeRequest;
}

/*! Returns the CHANNEL-NUMBER attribute. */
quint16 QXmppStunMessage::channelNumber() const
{
    return m_channelNumber;
}

/*!
    Sets the CHANNEL-NUMBER attribute to \a channelNumber.
*/
void QXmppStunMessage::setChannelNumber(quint16 channelNumber)
{
    m_channelNumber = channelNumber;
    m_attributes << ChannelNumber;
}

/*! Returns the DATA attribute. */
QByteArray QXmppStunMessage::data() const
{
    return m_data;
}

/*! Sets the DATA attribute. */
void QXmppStunMessage::setData(const QByteArray &data)
{
    m_data = data;
    m_attributes << DataAttr;
}

/*!
    Returns the LIFETIME attribute, indicating the duration in seconds for
    which the server will maintain an allocation.
*/
quint32 QXmppStunMessage::lifetime() const
{
    return m_lifetime;
}

/*!
    Sets the LIFETIME attribute to \a lifetime, indicating the duration in
    seconds for which the server will maintain an allocation.
*/
void QXmppStunMessage::setLifetime(quint32 lifetime)
{
    m_lifetime = lifetime;
    m_attributes << Lifetime;
}

/*! Returns the NONCE attribute. */
QByteArray QXmppStunMessage::nonce() const
{
    return m_nonce;
}

/*!
    Sets the NONCE attribute to \a nonce.
*/
void QXmppStunMessage::setNonce(const QByteArray &nonce)
{
    m_nonce = nonce;
    m_attributes << Nonce;
}

/*!
    Returns the PRIORITY attribute, the priority that would be assigned to
    a peer reflexive candidate discovered during the ICE check.
*/
quint32 QXmppStunMessage::priority() const
{
    return m_priority;
}

/*!
    Sets the PRIORITY attribute to \a priority, the priority that would be
    assigned to a peer reflexive candidate discovered during the ICE check.
*/
void QXmppStunMessage::setPriority(quint32 priority)
{
    m_priority = priority;
    m_attributes << Priority;
}

/*! Returns the REALM attribute. */
QString QXmppStunMessage::realm() const
{
    return m_realm;
}

/*!
    Sets the REALM attribute to \a realm.
*/
void QXmppStunMessage::setRealm(const QString &realm)
{
    m_realm = realm;
    m_attributes << Realm;
}

/*! Returns the REQUESTED-TRANSPORT attribute. */
quint8 QXmppStunMessage::requestedTransport() const
{
    return m_requestedTransport;
}

/*!
    Sets the REQUESTED-TRANSPORT attribute to \a requestedTransport.
*/
void QXmppStunMessage::setRequestedTransport(quint8 requestedTransport)
{
    m_requestedTransport = requestedTransport;
    m_attributes << RequestedTransport;
}

/*! Returns the RESERVATION-TOKEN attribute. */
QByteArray QXmppStunMessage::reservationToken() const
{
    return m_reservationToken;
}

/*!
    Sets the RESERVATION-TOKEN attribute to \a reservationToken.
*/
void QXmppStunMessage::setReservationToken(const QByteArray &reservationToken)
{
    m_reservationToken = reservationToken;
    m_reservationToken.resize(8);
    m_attributes << ReservationToken;
}

/*!
    Returns the SOFTWARE attribute, containing a textual description of the
    software being used.
*/
QString QXmppStunMessage::software() const
{
    return m_software;
}

/*!
    Sets the SOFTWARE attribute to \a software, containing a textual
    description of the software being used.
*/
void QXmppStunMessage::setSoftware(const QString &software)
{
    m_software = software;
    m_attributes << Software;
}

/*!
    Returns the USERNAME attribute, containing the username to use for
    authentication.
*/
QString QXmppStunMessage::username() const
{
    return m_username;
}

/*!
    Sets the USERNAME attribute to \a username, containing the username to use
    for authentication.
*/
void QXmppStunMessage::setUsername(const QString &username)
{
    m_username = username;
    m_attributes << Username;
}

/*!
    Decodes a QXmppStunMessage from \a buffer and checks its integrity using
    the given \a key. Decoding errors are appended to \a errors if provided.
*/
bool QXmppStunMessage::decode(const QByteArray &buffer, const QByteArray &key, QStringList *errors)
{
    QStringList silent;
    if (!errors) {
        errors = &silent;
    }

    if (buffer.size() < STUN_HEADER) {
        *errors << u"Received a truncated STUN packet"_s;
        return false;
    }

    // parse STUN header
    QDataStream stream(buffer);
    quint16 length;
    stream >> m_type;
    stream >> length;
    stream >> m_cookie;
    stream.readRawData(m_id.data(), m_id.size());

    if (length != buffer.size() - STUN_HEADER) {
        *errors << u"Received an invalid STUN packet"_s;
        return false;
    }

    // parse STUN attributes
    int done = 0;
    bool after_integrity = false;
    while (done < length) {
        quint16 a_type, a_length;
        stream >> a_type;
        stream >> a_length;
        const int pad_length = 4 * ((a_length + 3) / 4) - a_length;

        // only FINGERPRINT is allowed after MESSAGE-INTEGRITY
        if (after_integrity && a_type != Fingerprint) {
            *errors << u"Skipping attribute %1 after MESSAGE-INTEGRITY"_s.arg(QString::number(a_type));
            stream.skipRawData(a_length + pad_length);
            done += 4 + a_length + pad_length;
            continue;
        }

        if (a_type == Priority) {
            // PRIORITY
            if (a_length != sizeof(m_priority)) {
                return false;
            }
            stream >> m_priority;
            m_attributes << Priority;

        } else if (a_type == ErrorCode) {

            // ERROR-CODE
            if (a_length < 4) {
                return false;
            }
            quint16 reserved;
            quint8 errorCodeHigh, errorCodeLow;
            stream >> reserved;
            stream >> errorCodeHigh;
            stream >> errorCodeLow;
            errorCode = errorCodeHigh * 100 + errorCodeLow;
            QByteArray phrase(a_length - 4, 0);
            stream.readRawData(phrase.data(), phrase.size());
            errorPhrase = QString::fromUtf8(phrase);

        } else if (a_type == UseCandidate) {

            // USE-CANDIDATE
            if (a_length != 0) {
                return false;
            }
            useCandidate = true;

        } else if (a_type == ChannelNumber) {

            // CHANNEL-NUMBER
            if (a_length != 4) {
                return false;
            }
            stream >> m_channelNumber;
            stream.skipRawData(2);
            m_attributes << ChannelNumber;

        } else if (a_type == DataAttr) {

            // DATA
            m_data.resize(a_length);
            stream.readRawData(m_data.data(), m_data.size());
            m_attributes << DataAttr;

        } else if (a_type == Lifetime) {

            // LIFETIME
            if (a_length != sizeof(m_lifetime)) {
                return false;
            }
            stream >> m_lifetime;
            m_attributes << Lifetime;

        } else if (a_type == Nonce) {

            // NONCE
            m_nonce.resize(a_length);
            stream.readRawData(m_nonce.data(), m_nonce.size());
            m_attributes << Nonce;

        } else if (a_type == Realm) {

            // REALM
            QByteArray utf8Realm(a_length, 0);
            stream.readRawData(utf8Realm.data(), utf8Realm.size());
            m_realm = QString::fromUtf8(utf8Realm);
            m_attributes << Realm;

        } else if (a_type == RequestedTransport) {

            // REQUESTED-TRANSPORT
            if (a_length != 4) {
                return false;
            }
            stream >> m_requestedTransport;
            stream.skipRawData(3);
            m_attributes << RequestedTransport;

        } else if (a_type == ReservationToken) {

            // RESERVATION-TOKEN
            if (a_length != 8) {
                return false;
            }
            m_reservationToken.resize(a_length);
            stream.readRawData(m_reservationToken.data(), m_reservationToken.size());
            m_attributes << ReservationToken;

        } else if (a_type == Software) {

            // SOFTWARE
            QByteArray utf8Software(a_length, 0);
            stream.readRawData(utf8Software.data(), utf8Software.size());
            m_software = QString::fromUtf8(utf8Software);
            m_attributes << Software;

        } else if (a_type == Username) {

            // USERNAME
            QByteArray utf8Username(a_length, 0);
            stream.readRawData(utf8Username.data(), utf8Username.size());
            m_username = QString::fromUtf8(utf8Username);
            m_attributes << Username;

        } else if (a_type == MappedAddress) {

            // MAPPED-ADDRESS
            if (!decodeAddress(stream, a_length, mappedHost, mappedPort)) {
                *errors << u"Bad MAPPED-ADDRESS"_s;
                return false;
            }

        } else if (a_type == ChangeRequest) {

            // CHANGE-REQUEST
            if (a_length != sizeof(m_changeRequest)) {
                return false;
            }
            stream >> m_changeRequest;
            m_attributes << ChangeRequest;

        } else if (a_type == SourceAddress) {

            // SOURCE-ADDRESS
            if (!decodeAddress(stream, a_length, sourceHost, sourcePort)) {
                *errors << u"Bad SOURCE-ADDRESS"_s;
                return false;
            }

        } else if (a_type == ChangedAddress) {

            // CHANGED-ADDRESS
            if (!decodeAddress(stream, a_length, changedHost, changedPort)) {
                *errors << u"Bad CHANGED-ADDRESS"_s;
                return false;
            }

        } else if (a_type == OtherAddress) {

            // OTHER-ADDRESS
            if (!decodeAddress(stream, a_length, otherHost, otherPort)) {
                *errors << u"Bad OTHER-ADDRESS"_s;
                return false;
            }

        } else if (a_type == XorMappedAddress) {

            // XOR-MAPPED-ADDRESS
            if (!decodeAddress(stream, a_length, xorMappedHost, xorMappedPort, m_id)) {
                *errors << u"Bad XOR-MAPPED-ADDRESS"_s;
                return false;
            }

        } else if (a_type == XorPeerAddress) {

            // XOR-PEER-ADDRESS
            if (!decodeAddress(stream, a_length, xorPeerHost, xorPeerPort, m_id)) {
                *errors << u"Bad XOR-PEER-ADDRESS"_s;
                return false;
            }

        } else if (a_type == XorRelayedAddress) {

            // XOR-RELAYED-ADDRESS
            if (!decodeAddress(stream, a_length, xorRelayedHost, xorRelayedPort, m_id)) {
                *errors << u"Bad XOR-RELAYED-ADDRESS"_s;
                return false;
            }

        } else if (a_type == MessageIntegrity) {

            // MESSAGE-INTEGRITY
            if (a_length != 20) {
                return false;
            }
            QByteArray integrity(20, 0);
            stream.readRawData(integrity.data(), integrity.size());

            // check HMAC-SHA1
            if (!key.isEmpty()) {
                QByteArray copy = buffer.left(STUN_HEADER + done);
                setBodyLength(copy, done + 24);
                if (integrity != QXmppUtils::generateHmacSha1(key, copy)) {
                    *errors << u"Bad message integrity"_s;
                    return false;
                }
            }

            // from here onwards, only FINGERPRINT is allowed
            after_integrity = true;

        } else if (a_type == Fingerprint) {

            // FINGERPRINT
            if (a_length != 4) {
                return false;
            }
            quint32 fingerprint;
            stream >> fingerprint;

            // check CRC32
            QByteArray copy = buffer.left(STUN_HEADER + done);
            setBodyLength(copy, done + 8);
            const quint32 expected = QXmppUtils::generateCrc32(copy) ^ 0x5354554eL;
            if (fingerprint != expected) {
                *errors << u"Bad fingerprint"_s;
                return false;
            }

            // stop parsing, no more attributes are allowed
            return true;

        } else if (a_type == IceControlling) {

            // ICE-CONTROLLING
            if (a_length != 8) {
                return false;
            }
            iceControlling.resize(a_length);
            stream.readRawData(iceControlling.data(), iceControlling.size());

        } else if (a_type == IceControlled) {

            // ICE-CONTROLLED
            if (a_length != 8) {
                return false;
            }
            iceControlled.resize(a_length);
            stream.readRawData(iceControlled.data(), iceControlled.size());

        } else {

            // Unknown attribute
            stream.skipRawData(a_length);
            *errors << u"Skipping unknown attribute %1"_s.arg(QString::number(a_type));
        }
        stream.skipRawData(pad_length);
        done += 4 + a_length + pad_length;
    }
    return true;
}

/*!
    Encodes the current QXmppStunMessage, optionally calculating the
    message integrity attribute using the given \a key. If \a addFingerprint is
    true, the FINGERPRINT attribute is included.
*/
QByteArray QXmppStunMessage::encode(const QByteArray &key, bool addFingerprint) const
{
    QByteArray buffer;
    QDataStream stream(&buffer, QIODevice::WriteOnly);

    // encode STUN header
    quint16 length = 0;
    stream << m_type;
    stream << length;
    stream << m_cookie;
    stream.writeRawData(m_id.data(), m_id.size());

    // MAPPED-ADDRESS
    addAddress(stream, MappedAddress, mappedHost, mappedPort);

    // CHANGE-REQUEST
    if (m_attributes.contains(ChangeRequest)) {
        stream << quint16(ChangeRequest);
        stream << quint16(sizeof(m_changeRequest));
        stream << m_changeRequest;
    }

    // SOURCE-ADDRESS
    addAddress(stream, SourceAddress, sourceHost, sourcePort);

    // CHANGED-ADDRESS
    addAddress(stream, ChangedAddress, changedHost, changedPort);

    // OTHER-ADDRESS
    addAddress(stream, OtherAddress, otherHost, otherPort);

    // XOR-MAPPED-ADDRESS
    addAddress(stream, XorMappedAddress, xorMappedHost, xorMappedPort, m_id);

    // XOR-PEER-ADDRESS
    addAddress(stream, XorPeerAddress, xorPeerHost, xorPeerPort, m_id);

    // XOR-RELAYED-ADDRESS
    addAddress(stream, XorRelayedAddress, xorRelayedHost, xorRelayedPort, m_id);

    // ERROR-CODE
    if (errorCode) {
        const quint16 reserved = 0;
        const quint8 errorCodeHigh = errorCode / 100;
        const quint8 errorCodeLow = errorCode % 100;
        const QByteArray phrase = errorPhrase.toUtf8();
        stream << quint16(ErrorCode);
        stream << quint16(phrase.size() + 4);
        stream << reserved;
        stream << errorCodeHigh;
        stream << errorCodeLow;
        stream.writeRawData(phrase.data(), phrase.size());
        if (phrase.size() % 4) {
            const QByteArray padding(4 - (phrase.size() % 4), 0);
            stream.writeRawData(padding.data(), padding.size());
        }
    }

    // PRIORITY
    if (m_attributes.contains(Priority)) {
        stream << quint16(Priority);
        stream << quint16(sizeof(m_priority));
        stream << m_priority;
    }

    // USE-CANDIDATE
    if (useCandidate) {
        stream << quint16(UseCandidate);
        stream << quint16(0);
    }

    // CHANNEL-NUMBER
    if (m_attributes.contains(ChannelNumber)) {
        stream << quint16(ChannelNumber);
        stream << quint16(4);
        stream << m_channelNumber;
        stream << quint16(0);
    }

    // DATA
    if (m_attributes.contains(DataAttr)) {
        stream << quint16(DataAttr);
        stream << quint16(m_data.size());
        stream.writeRawData(m_data.data(), m_data.size());
        if (m_data.size() % 4) {
            const QByteArray padding(4 - (m_data.size() % 4), 0);
            stream.writeRawData(padding.data(), padding.size());
        }
    }

    // LIFETIME
    if (m_attributes.contains(Lifetime)) {
        stream << quint16(Lifetime);
        stream << quint16(sizeof(m_lifetime));
        stream << m_lifetime;
    }

    // NONCE
    if (m_attributes.contains(Nonce)) {
        stream << quint16(Nonce);
        stream << quint16(m_nonce.size());
        stream.writeRawData(m_nonce.data(), m_nonce.size());
        if (m_nonce.size() % 4) {
            const QByteArray padding(4 - (m_nonce.size() % 4), 0);
            stream.writeRawData(padding.data(), padding.size());
        }
    }

    // REALM
    if (m_attributes.contains(Realm)) {
        encodeString(stream, Realm, m_realm);
    }

    // REQUESTED-TRANSPORT
    if (m_attributes.contains(RequestedTransport)) {
        const QByteArray reserved(3, 0);
        stream << quint16(RequestedTransport);
        stream << quint16(4);
        stream << m_requestedTransport;
        stream.writeRawData(reserved.data(), reserved.size());
    }

    // RESERVATION-TOKEN
    if (m_attributes.contains(ReservationToken)) {
        stream << quint16(ReservationToken);
        stream << quint16(m_reservationToken.size());
        stream.writeRawData(m_reservationToken.data(), m_reservationToken.size());
    }

    // SOFTWARE
    if (m_attributes.contains(Software)) {
        encodeString(stream, Software, m_software);
    }

    // USERNAME
    if (m_attributes.contains(Username)) {
        encodeString(stream, Username, m_username);
    }

    // ICE-CONTROLLING or ICE-CONTROLLED
    if (!iceControlling.isEmpty()) {
        stream << quint16(IceControlling);
        stream << quint16(iceControlling.size());
        stream.writeRawData(iceControlling.data(), iceControlling.size());
    } else if (!iceControlled.isEmpty()) {
        stream << quint16(IceControlled);
        stream << quint16(iceControlled.size());
        stream.writeRawData(iceControlled.data(), iceControlled.size());
    }

    // set body length
    setBodyLength(buffer, buffer.size() - STUN_HEADER);

    // MESSAGE-INTEGRITY
    if (!key.isEmpty()) {
        setBodyLength(buffer, buffer.size() - STUN_HEADER + 24);
        QByteArray integrity = QXmppUtils::generateHmacSha1(key, buffer);
        stream << quint16(MessageIntegrity);
        stream << quint16(integrity.size());
        stream.writeRawData(integrity.data(), integrity.size());
    }

    // FINGERPRINT
    if (addFingerprint) {
        setBodyLength(buffer, buffer.size() - STUN_HEADER + 8);
        quint32 fingerprint = QXmppUtils::generateCrc32(buffer) ^ 0x5354554eL;
        stream << quint16(Fingerprint);
        stream << quint16(sizeof(fingerprint));
        stream << fingerprint;
    }

    return buffer;
}

/*!
    If the given packet \a buffer looks like a STUN message, returns the
    message type and writes \a cookie and \a id; otherwise returns 0.
*/
quint16 QXmppStunMessage::peekType(const QByteArray &buffer, quint32 &cookie, QByteArray &id)
{
    if (buffer.size() < STUN_HEADER) {
        return 0;
    }

    // parse STUN header
    QDataStream stream(buffer);
    quint16 type;
    quint16 length;
    stream >> type;
    stream >> length;
    stream >> cookie;

    if (length != buffer.size() - STUN_HEADER) {
        return 0;
    }

    id.resize(STUN_ID_SIZE);
    stream.readRawData(id.data(), id.size());
    return type;
}

QString QXmppStunMessage::toString() const
{
    QStringList dumpLines;
    QString typeName;
    switch (messageMethod()) {
    case Binding:
        typeName = u"Binding"_s;
        break;
    case SharedSecret:
        typeName = u"Shared Secret"_s;
        break;
    case Allocate:
        typeName = u"Allocate"_s;
        break;
    case Refresh:
        typeName = u"Refresh"_s;
        break;
    case Send:
        typeName = u"Send"_s;
        break;
    case Data:
        typeName = u"Data"_s;
        break;
    case CreatePermission:
        typeName = u"CreatePermission"_s;
        break;
    case ChannelBind:
        typeName = u"ChannelBind"_s;
        break;
    default:
        typeName = u"Unknown"_s;
        break;
    }
    switch (messageClass()) {
    case Request:
        typeName += u" Request"_s;
        break;
    case Indication:
        typeName += u" Indication"_s;
        break;
    case Response:
        typeName += u" Response"_s;
        break;
    case Error:
        typeName += u" Error"_s;
        break;
    default:
        break;
    }
    dumpLines << u" type %1 (%2)"_s
                     .arg(typeName, QString::number(m_type));
    dumpLines << u" id %1"_s.arg(QString::fromLatin1(m_id.toHex()));

    // attributes
    if (m_attributes.contains(ChannelNumber)) {
        dumpLines << u" * CHANNEL-NUMBER %1"_s.arg(QString::number(m_channelNumber));
    }
    if (errorCode) {
        dumpLines << u" * ERROR-CODE %1 %2"_s
                         .arg(QString::number(errorCode), errorPhrase);
    }
    if (m_attributes.contains(Lifetime)) {
        dumpLines << u" * LIFETIME %1"_s.arg(QString::number(m_lifetime));
    }
    if (m_attributes.contains(Nonce)) {
        dumpLines << u" * NONCE %1"_s.arg(QString::fromLatin1(m_nonce));
    }
    if (m_attributes.contains(Realm)) {
        dumpLines << u" * REALM %1"_s.arg(m_realm);
    }
    if (m_attributes.contains(RequestedTransport)) {
        dumpLines << u" * REQUESTED-TRANSPORT 0x%1"_s.arg(QString::number(m_requestedTransport, 16));
    }
    if (m_attributes.contains(ReservationToken)) {
        dumpLines << u" * RESERVATION-TOKEN %1"_s.arg(QString::fromLatin1(m_reservationToken.toHex()));
    }
    if (m_attributes.contains(Software)) {
        dumpLines << u" * SOFTWARE %1"_s.arg(m_software);
    }
    if (m_attributes.contains(Username)) {
        dumpLines << u" * USERNAME %1"_s.arg(m_username);
    }
    if (mappedPort) {
        dumpLines << u" * MAPPED-ADDRESS %1 %2"_s
                         .arg(mappedHost.toString(), QString::number(mappedPort));
    }
    if (m_attributes.contains(ChangeRequest)) {
        dumpLines << u" * CHANGE-REQUEST %1"_s
                         .arg(QString::number(m_changeRequest));
    }
    if (sourcePort) {
        dumpLines << u" * SOURCE-ADDRESS %1 %2"_s
                         .arg(sourceHost.toString(), QString::number(sourcePort));
    }
    if (changedPort) {
        dumpLines << u" * CHANGED-ADDRESS %1 %2"_s
                         .arg(changedHost.toString(), QString::number(changedPort));
    }
    if (otherPort) {
        dumpLines << u" * OTHER-ADDRESS %1 %2"_s
                         .arg(otherHost.toString(), QString::number(otherPort));
    }
    if (xorMappedPort) {
        dumpLines << u" * XOR-MAPPED-ADDRESS %1 %2"_s
                         .arg(xorMappedHost.toString(), QString::number(xorMappedPort));
    }
    if (xorPeerPort) {
        dumpLines << u" * XOR-PEER-ADDRESS %1 %2"_s
                         .arg(xorPeerHost.toString(), QString::number(xorPeerPort));
    }
    if (xorRelayedPort) {
        dumpLines << u" * XOR-RELAYED-ADDRESS %1 %2"_s
                         .arg(xorRelayedHost.toString(), QString::number(xorRelayedPort));
    }
    if (m_attributes.contains(Priority)) {
        dumpLines << u" * PRIORITY %1"_s.arg(QString::number(m_priority));
    }
    if (!iceControlling.isEmpty()) {
        dumpLines << u" * ICE-CONTROLLING %1"_s
                         .arg(QString::fromLatin1(iceControlling.toHex()));
    }
    if (!iceControlled.isEmpty()) {
        dumpLines << u" * ICE-CONTROLLED %1"_s
                         .arg(QString::fromLatin1(iceControlled.toHex()));
    }
    if (useCandidate) {
        dumpLines << u" * USE-CANDIDATE"_s;
    }

    return dumpLines.join(u'\n');
}

QXmppStunTransaction::QXmppStunTransaction(const QXmppStunMessage &request,
                                           std::function<void(const QXmppStunMessage &)> sendStunMessage,
                                           std::function<void(QXmppStunTransaction *)> onFinished,
                                           QObject *parent)
    : QXmppLoggable(parent),
      m_request(request)
{
    connect(this, &QXmppStunTransaction::writeStun, this, sendStunMessage);
    connect(this, &QXmppStunTransaction::finished, this, [this, onFinished = std::move(onFinished)]() {
        onFinished(this);
    });

    // RTO timer
    m_retryTimer = new QTimer(this);
    m_retryTimer->setSingleShot(true);
    connect(m_retryTimer, &QTimer::timeout, this, &QXmppStunTransaction::retry);

    // send packet immediately
    m_retryTimer->start(0);
}

void QXmppStunTransaction::readStun(const QXmppStunMessage &response)
{
    if (response.messageClass() == QXmppStunMessage::Error ||
        response.messageClass() == QXmppStunMessage::Response) {
        m_response = response;
        m_retryTimer->stop();
        Q_EMIT finished();
    }
}

void QXmppStunTransaction::retry()
{
    if (m_tries >= STUN_RTO_MAX) {
        m_response.setType(QXmppStunMessage::Error);
        m_response.errorPhrase = u"Request timed out"_s;
        Q_EMIT finished();
        return;
    }

    // resend request
    Q_EMIT writeStun(m_request);
    m_retryTimer->start(m_tries ? 2 * m_retryTimer->interval() : STUN_RTO_INTERVAL);
    m_tries++;
}

/*!
    Constructs a new QXmppTurnAllocation with \a parent.
*/
QXmppTurnAllocation::QXmppTurnAllocation(QObject *parent)
    : QXmppIceTransport(parent)
{
    socket = new QUdpSocket(this);
    socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
    connect(socket, &QIODevice::readyRead,
            this, &QXmppTurnAllocation::readyRead);

    m_timer = new QTimer(this);
    m_timer->setSingleShot(true);
    connect(m_timer, &QTimer::timeout,
            this, &QXmppTurnAllocation::refresh);

    // channels are valid 600s, we refresh every 500s
    m_channelTimer = new QTimer(this);
    m_channelTimer->setInterval(500 * 1000);
    connect(m_channelTimer, &QTimer::timeout,
            this, &QXmppTurnAllocation::refreshChannels);
}

/*! Destroys the TURN allocation. */
QXmppTurnAllocation::~QXmppTurnAllocation()
{
    if (m_state == ConnectedState) {
        disconnectFromHost();
    }
}

/*! Allocates the TURN allocation. */
void QXmppTurnAllocation::connectToHost()
{
    if (m_state != UnconnectedState) {
        return;
    }

    // start listening for UDP
    if (socket->state() == QAbstractSocket::UnconnectedState) {
        if (!socket->bind()) {
            warning(u"Could not start listening for TURN"_s);
            return;
        }
    }

    // send allocate request
    QXmppStunMessage request;
    request.setType(int(QXmppStunMessage::Allocate) | int(QXmppStunMessage::Request));
    request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
    request.setLifetime(m_lifetime);
    request.setRequestedTransport(0x11);
    sendStunMessage(std::move(request));

    // update state
    setState(ConnectingState);
}

/*! Releases the TURN allocation. */
void QXmppTurnAllocation::disconnectFromHost()
{
    m_channelTimer->stop();
    m_timer->stop();

    // clear channels and any outstanding transactions
    m_channels.clear();
    qDeleteAll(m_transactions);
    m_transactions.clear();

    // end allocation
    if (m_state == ConnectedState) {
        QXmppStunMessage request;
        request.setType(int(QXmppStunMessage::Refresh) | int(QXmppStunMessage::Request));
        request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
        request.setNonce(m_nonce);
        request.setRealm(m_realm);
        request.setUsername(m_username);
        request.setLifetime(0);
        sendStunMessage(std::move(request));

        setState(ClosingState);
    } else {
        setState(UnconnectedState);
    }
}

void QXmppTurnAllocation::sendStunMessage(QXmppStunMessage &&message)
{
    m_transactions << new QXmppStunTransaction(
        message,
        [this](const auto &stunMessage) { writeStun(stunMessage); },
        [this](QXmppStunTransaction *transaction) {
            m_transactions.removeAll(transaction);
            transaction->deleteLater();
            handleTransactionFinished(transaction);
        },
        this);
}

QXmppJingleCandidate QXmppTurnAllocation::localCandidate(int component) const
{
    QXmppJingleCandidate candidate;
    candidate.setComponent(component);
    candidate.setHost(relayedHost());
    candidate.setId(QXmppUtils::generateStanzaHash(10));
    candidate.setPort(relayedPort());
    candidate.setProtocol(u"udp"_s);
    candidate.setType(QXmppJingleCandidate::RelayedType);
    candidate.setPriority(candidatePriority(candidate));
    candidate.setFoundation(computeFoundation(
        candidate.type(),
        candidate.protocol(),
        candidate.host()));
    return candidate;
}

void QXmppTurnAllocation::readyRead()
{
    QByteArray buffer;
    QHostAddress remoteHost;
    quint16 remotePort;
    while (socket->hasPendingDatagrams()) {
        const qint64 size = socket->pendingDatagramSize();
        buffer.resize(size);
        socket->readDatagram(buffer.data(), buffer.size(), &remoteHost, &remotePort);
        handleDatagram(buffer, remoteHost, remotePort);
    }
}

void QXmppTurnAllocation::handleDatagram(const QByteArray &buffer, const QHostAddress &remoteHost, quint16 remotePort)
{
    // demultiplex channel data
    if (buffer.size() >= 4 && (buffer[0] & 0xc0) == 0x40) {
        QDataStream stream(buffer);
        quint16 channel, length;
        stream >> channel;
        stream >> length;
        if (m_state == ConnectedState && m_channels.contains(channel) && length <= buffer.size() - 4) {
            Q_EMIT datagramReceived(buffer.mid(4, length), m_channels[channel].first,
                                    m_channels[channel].second);
        }
        return;
    }

    // parse STUN message
    QXmppStunMessage message;
    QStringList errors;
    if (!message.decode(buffer, QByteArray(), &errors)) {
        for (const auto &error : std::as_const(errors)) {
            warning(error);
        }
        return;
    }

#ifdef QXMPP_DEBUG_STUN
    logReceived(u"TURN packet from %1 port %2\n%3"_s.arg(remoteHost.toString(), QString::number(remotePort), message.toString()));
#endif

    // find transaction
    for (auto *transaction : std::as_const(m_transactions)) {
        if (transaction->request().id() == message.id() &&
            transaction->request().messageMethod() == message.messageMethod()) {
            transaction->readStun(message);
            return;
        }
    }

    // Data Indication packets (relayed data encoded in STUN messages)
    // AFAIK data indication STUN packets are used initially and then the more optimized Channel
    // Data packets are used (which only have 4 additional bytes for channel number and length).
    if (message.messageMethod() == QXmppStunMessage::Data && message.messageClass() == QXmppStunMessage::Indication) {
        debug(u"TURN relayed data via Data Indication received"_s);
        Q_EMIT datagramReceived(message.data(), message.xorPeerHost, message.xorPeerPort);
    } else {
        warning(u"Unexpected TURN packet received."_s);
    }
}

/*! Refresh allocation. */
void QXmppTurnAllocation::refresh()
{
    QXmppStunMessage request;
    request.setType(int(QXmppStunMessage::Refresh) | int(QXmppStunMessage::Request));
    request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
    request.setNonce(m_nonce);
    request.setRealm(m_realm);
    request.setUsername(m_username);
    sendStunMessage(std::move(request));
}

/*! Refresh channel bindings. */
void QXmppTurnAllocation::refreshChannels()
{
    for (auto itr = m_channels.cbegin(); itr != m_channels.cend(); itr++) {
        QXmppStunMessage request;
        request.setType(int(QXmppStunMessage::ChannelBind) | int(QXmppStunMessage::Request));
        request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
        request.setNonce(m_nonce);
        request.setRealm(m_realm);
        request.setUsername(m_username);
        request.setChannelNumber(itr.key());
        request.xorPeerHost = itr.value().first;
        request.xorPeerPort = itr.value().second;
        sendStunMessage(std::move(request));
    }
}

/*!
    Returns the relayed host address, i.e. the address on the server
    used to communicate with peers.
*/
QHostAddress QXmppTurnAllocation::relayedHost() const
{
    return m_relayedHost;
}

/*!
    Returns the relayed port, i.e. the port on the server used to communicate
    with peers.
*/
quint16 QXmppTurnAllocation::relayedPort() const
{
    return m_relayedPort;
}

/*!
    Sets the \a password used to authenticate with the TURN server.
*/
void QXmppTurnAllocation::setPassword(const QString &password)
{
    m_password = password;
}

/*!
    Sets the TURN server to use.

    \a host is the address of the TURN server. \a port is the port of the TURN
    server.
*/
void QXmppTurnAllocation::setServer(const QHostAddress &host, quint16 port)
{
    m_turnHost = host;
    m_turnPort = port;
}

/*!
    Sets the \a user used for authentication with the TURN server.
*/
void QXmppTurnAllocation::setUser(const QString &user)
{
    m_username = user;
}

/*! Returns the current state of the allocation. */
QXmppTurnAllocation::AllocationState QXmppTurnAllocation::state() const
{
    return m_state;
}

void QXmppTurnAllocation::setState(AllocationState state)
{
    if (state == m_state) {
        return;
    }
    m_state = state;
    if (m_state == ConnectedState) {
        Q_EMIT connected();
    } else if (m_state == UnconnectedState) {
        m_timer->stop();
        Q_EMIT disconnected();
    }
}

void QXmppTurnAllocation::handleTransactionFinished(QXmppStunTransaction *transaction)
{
    // handle authentication
    const QXmppStunMessage reply = transaction->response();
    if (reply.messageClass() == QXmppStunMessage::Error &&
        reply.errorCode == 401 &&
        (reply.nonce() != m_nonce && reply.realm() != m_realm)) {
        // update long-term credentials
        m_nonce = reply.nonce();
        m_realm = reply.realm();
        QCryptographicHash hash(QCryptographicHash::Md5);
        hash.addData((m_username + u':' + m_realm + u':' + m_password).toUtf8());
        m_key = hash.result();

        // retry request
        QXmppStunMessage request(transaction->request());
        request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
        request.setNonce(m_nonce);
        request.setRealm(m_realm);
        request.setUsername(m_username);
        sendStunMessage(std::move(request));
        return;
    }

    const quint16 method = transaction->request().messageMethod();
    if (method == QXmppStunMessage::Allocate) {

        if (reply.messageClass() == QXmppStunMessage::Error) {
            warning(u"Allocation failed: %1 %2"_s.arg(QString::number(reply.errorCode), reply.errorPhrase));
            setState(UnconnectedState);
            return;
        }
        if (reply.xorRelayedHost.isNull() ||
            reply.xorRelayedHost.protocol() != QAbstractSocket::IPv4Protocol ||
            !reply.xorRelayedPort) {
            warning(u"Allocation did not yield a valid relayed address"_s);
            setState(UnconnectedState);
            return;
        }

        // store relayed address
        m_relayedHost = reply.xorRelayedHost;
        m_relayedPort = reply.xorRelayedPort;

        // schedule refresh
        m_lifetime = reply.lifetime();
        m_timer->start((m_lifetime - 60) * 1000);

        setState(ConnectedState);

    } else if (method == QXmppStunMessage::ChannelBind) {

        if (reply.messageClass() == QXmppStunMessage::Error) {
            warning(u"ChannelBind failed: %1 %2"_s.arg(QString::number(reply.errorCode), reply.errorPhrase));

            // remove channel
            m_channels.remove(transaction->request().channelNumber());
            if (m_channels.isEmpty()) {
                m_channelTimer->stop();
            }
            return;
        }

    } else if (method == QXmppStunMessage::Refresh) {

        if (reply.messageClass() == QXmppStunMessage::Error) {
            warning(u"Refresh failed: %1 %2"_s.arg(QString::number(reply.errorCode), reply.errorPhrase));
            setState(UnconnectedState);
            return;
        }

        if (m_state == ClosingState) {
            setState(UnconnectedState);
            return;
        }

        // schedule refresh
        m_lifetime = reply.lifetime();
        m_timer->start((m_lifetime - 60) * 1000);
    }
}

qint64 QXmppTurnAllocation::writeDatagram(const QByteArray &data, const QHostAddress &host, quint16 port)
{
    if (m_state != ConnectedState) {
        return -1;
    }

    const Address addr = qMakePair(host, port);
    quint16 channel = m_channels.key(addr);

    if (!channel) {
        channel = m_channelNumber++;
        m_channels.insert(channel, addr);

        // bind channel
        QXmppStunMessage request;
        request.setType(int(QXmppStunMessage::ChannelBind) | int(QXmppStunMessage::Request));
        request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
        request.setNonce(m_nonce);
        request.setRealm(m_realm);
        request.setUsername(m_username);
        request.setChannelNumber(channel);
        request.xorPeerHost = host;
        request.xorPeerPort = port;
        sendStunMessage(std::move(request));

        // schedule refresh
        if (!m_channelTimer->isActive()) {
            m_channelTimer->start();
        }
    }

    // send data
    QByteArray channelData;
    channelData.reserve(4 + data.size());
    QDataStream stream(&channelData, QIODevice::WriteOnly);
    stream << channel;
    stream << quint16(data.size());
    stream.writeRawData(data.data(), data.size());
    if (socket->writeDatagram(channelData, m_turnHost, m_turnPort) == channelData.size()) {
        return data.size();
    } else {
        return -1;
    }
}

void QXmppTurnAllocation::writeStun(const QXmppStunMessage &message)
{
    socket->writeDatagram(message.encode(m_key), m_turnHost, m_turnPort);
#ifdef QXMPP_DEBUG_STUN
    logSent(u"TURN packet to %1 port %2\n%3"_s.arg(m_turnHost.toString(), QString::number(m_turnPort), message.toString()));
#endif
}

QXmppUdpTransport::QXmppUdpTransport(QUdpSocket *socket, QObject *parent)
    : QXmppIceTransport(parent), m_socket(socket)
{
    connect(m_socket, &QIODevice::readyRead, this, &QXmppUdpTransport::readyRead);
}

QXmppUdpTransport::~QXmppUdpTransport() = default;

void QXmppUdpTransport::disconnectFromHost()
{
    m_socket->close();
}

QXmppJingleCandidate QXmppUdpTransport::localCandidate(int component) const
{
    QXmppJingleCandidate candidate;
    candidate.setComponent(component);
    // remove scope ID from IPv6 non-link local addresses
    QHostAddress addr(m_socket->localAddress());
    if (addr.protocol() == QAbstractSocket::IPv6Protocol &&
        !isIPv6LinkLocalAddress(addr)) {
        addr.setScopeId(QString());
    }
    candidate.setHost(addr);
    candidate.setId(QXmppUtils::generateStanzaHash(10));
    candidate.setPort(m_socket->localPort());
    candidate.setProtocol(u"udp"_s);
    candidate.setType(QXmppJingleCandidate::HostType);
    candidate.setPriority(candidatePriority(candidate));
    candidate.setFoundation(computeFoundation(
        candidate.type(),
        candidate.protocol(),
        candidate.host()));
    return candidate;
}

void QXmppUdpTransport::readyRead()
{
    QByteArray buffer;
    QHostAddress remoteHost;
    quint16 remotePort;
    while (m_socket->hasPendingDatagrams()) {
        const qint64 size = m_socket->pendingDatagramSize();
        buffer.resize(size);
        m_socket->readDatagram(buffer.data(), buffer.size(), &remoteHost, &remotePort);
        Q_EMIT datagramReceived(buffer, remoteHost, remotePort);
    }
}

qint64 QXmppUdpTransport::writeDatagram(const QByteArray &data, const QHostAddress &host, quint16 port)
{
    QHostAddress remoteHost = host;
    if (isIPv6LinkLocalAddress(host)) {
        remoteHost.setScopeId(m_socket->localAddress().scopeId());
    }
    return m_socket->writeDatagram(data, remoteHost, port);
}

class CandidatePair : public QXmppLoggable
{
public:
    enum State {
        FrozenState,
        WaitingState,
        InProgressState,
        SucceededState,
        FailedState
    };

    CandidatePair(int component, bool controlling, QObject *parent)
        : QXmppLoggable(parent), m_component(component), m_controlling(controlling) { }

    quint64 priority() const;
    State state() const;
    void setState(State state);
    QString toString() const;

    bool nominated = false;
    bool nominating = false;
    QXmppJingleCandidate remote;
    QXmppJingleCandidate reflexive;
    QXmppIceTransport *transport = nullptr;
    QXmppStunTransaction *transaction = nullptr;

private:
    int m_component;
    bool m_controlling;
    State m_state = WaitingState;
};

static bool candidatePairPtrLessThan(const CandidatePair *p1, const CandidatePair *p2)
{
    return p1->priority() > p2->priority();
}

quint64 CandidatePair::priority() const
{
    const QXmppJingleCandidate local = transport->localCandidate(m_component);

    // see RFC 5245 - 5.7.2. Computing Pair Priority and Ordering Pairs
    const quint32 G = m_controlling ? local.priority() : remote.priority();
    const quint32 D = m_controlling ? remote.priority() : local.priority();
    return (quint64(1) << 32) * qMin(G, D) + 2 * qMax(G, D) + (G > D ? 1 : 0);
}

CandidatePair::State CandidatePair::state() const
{
    return m_state;
}

void CandidatePair::setState(CandidatePair::State state)
{
    m_state = state;
    info(u"ICE pair changed to state %1 %2"_s.arg(QLatin1String(pair_states[state]), toString()));
}

QString CandidatePair::toString() const
{
    const QXmppJingleCandidate candidate = transport->localCandidate(m_component);
    QString str = u"%1 port %2"_s.arg(remote.host().toString(), QString::number(remote.port()));
    if (candidate.type() == QXmppJingleCandidate::HostType) {
        str += u" (local %1 port %2)"_s.arg(candidate.host().toString(), QString::number(candidate.port()));
    } else {
        str += u" (relayed)"_s;
    }
    if (!reflexive.host().isNull() && reflexive.port()) {
        str += u" (reflexive %1 port %2)"_s.arg(reflexive.host().toString(), QString::number(reflexive.port()));
    }
    return str;
}

class QXmppIcePrivate
{
public:
    QXmppIcePrivate();

    bool iceControlling = false;
    QString localUser;
    QString localPassword;
    QString remoteUser;
    QString remotePassword;
    QList<StunServer> stunServers;
    QByteArray tieBreaker;
};

QXmppIcePrivate::QXmppIcePrivate()
    : localUser(QXmppUtils::generateStanzaHash(4)),
      localPassword(QXmppUtils::generateStanzaHash(22)),
      tieBreaker(QXmppUtils::generateRandomBytes(8))
{
}

struct QXmppIceTransportDetails {
    QXmppIceTransport *transport;
    StunServer stunServer;
};

class QXmppIceComponentPrivate
{
public:
    QXmppIceComponentPrivate(int component_, QXmppIcePrivate *config_, QXmppIceComponent *qq)
        : component(component_), config(config_), q(qq) { }
    bool addRemoteCandidate(const QXmppJingleCandidate &candidate);
    void performCheck(CandidatePair *pair, bool nominate);
    void setSockets(QList<QUdpSocket *> sockets);
    void setTurnServer(const QHostAddress &host, quint16 port);
    void setTurnUser(const QString &user);
    void setTurnPassword(const QString &password);
    void sendStunMessage(const QXmppStunMessage &message, QXmppIceTransport *transport, const StunServer &stunServer);
    void writeStun(const QXmppStunMessage &message, QXmppIceTransport *transport, const QHostAddress &remoteHost, quint16 remotePort);

    CandidatePair *activePair = nullptr;
    const int component;
    const QXmppIcePrivate *const config;
    CandidatePair *fallbackPair = nullptr;

    QXmppIceConnection::GatheringState gatheringState = QXmppIceConnection::NewGatheringState;

    QList<QXmppJingleCandidate> localCandidates;

    quint32 peerReflexivePriority = 0;
    QList<QXmppJingleCandidate> remoteCandidates;

    QList<CandidatePair *> pairs;
    QList<QXmppIceTransport *> transports;
    QTimer *timer = nullptr;

    // STUN server
    std::vector<std::tuple<QXmppStunTransaction *, QXmppIceTransport *>> stunTransactions;

    // TURN server
    QXmppTurnAllocation *turnAllocation = nullptr;
    bool turnConfigured = false;

private:
    QXmppIceComponent *q;
};

bool QXmppIceComponentPrivate::addRemoteCandidate(const QXmppJingleCandidate &candidate)
{
    if (candidate.component() != component ||
        (candidate.type() != QXmppJingleCandidate::HostType &&
         candidate.type() != QXmppJingleCandidate::RelayedType &&
         candidate.type() != QXmppJingleCandidate::ServerReflexiveType) ||
        candidate.protocol() != u"udp" ||
        (candidate.host().protocol() != QAbstractSocket::IPv4Protocol &&
         candidate.host().protocol() != QAbstractSocket::IPv6Protocol)) {
        return false;
    }

    for (const auto &c : std::as_const(remoteCandidates)) {
        if (c.host() == candidate.host() && c.port() == candidate.port()) {
            return false;
        }
    }
    remoteCandidates << candidate;

    for (auto *transport : std::as_const(transports)) {
        // only pair compatible addresses
        const QXmppJingleCandidate local = transport->localCandidate(component);
        if (!isCompatibleAddress(local.host(), candidate.host())) {
            continue;
        }

        auto *pair = new CandidatePair(component, config->iceControlling, q);
        pair->remote = candidate;
        pair->transport = transport;
        pairs << pair;

        if (!fallbackPair && local.type() == QXmppJingleCandidate::HostType) {
            fallbackPair = pair;
        }
    }

    std::sort(pairs.begin(), pairs.end(), candidatePairPtrLessThan);

    return true;
}

void QXmppIceComponentPrivate::performCheck(CandidatePair *pair, bool nominate)
{
    QXmppStunMessage message;
    message.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
    message.setType(int(QXmppStunMessage::Binding) | int(QXmppStunMessage::Request));
    message.setPriority(peerReflexivePriority);
    message.setUsername(u"%1:%2"_s.arg(config->remoteUser, config->localUser));
    if (config->iceControlling) {
        message.iceControlling = config->tieBreaker;
        message.useCandidate = true;
    } else {
        message.iceControlled = config->tieBreaker;
    }
    pair->nominating = nominate;
    pair->setState(CandidatePair::InProgressState);
    pair->transaction = new QXmppStunTransaction(
        message,
        [this, transport = pair->transport, remote = pair->remote](const auto &message) {
            writeStun(message, transport, remote.host(), remote.port());
        },
        [this, pair = QPointer(pair)](QXmppStunTransaction *transaction) {
            transaction->deleteLater();

            if (!pair) {
                return;
            }
            const auto &response = transaction->response();
            if (response.messageClass() == QXmppStunMessage::Response) {
                // store peer-reflexive address
                if (!response.xorMappedHost.isNull() && response.xorMappedPort != 0) {
                    pair->reflexive.setHost(response.xorMappedHost);
                    pair->reflexive.setPort(response.xorMappedPort);
                }

                pair->setState(CandidatePair::SucceededState);
                if (pair->nominating) {
                    // outgoing media can flow
                    pair->nominated = true;
                }
            } else {
                q->debug(u"ICE forward check failed %1 (error %2)"_s.arg(pair->toString(), transaction->response().errorPhrase));
                pair->setState(CandidatePair::FailedState);
            }
            pair->transaction = nullptr;
        },
        q);
}

void QXmppIceComponentPrivate::setSockets(QList<QUdpSocket *> sockets)
{
    // clear previous candidates and sockets
    localCandidates.clear();
    qDeleteAll(pairs);
    for (auto *transport : std::as_const(transports)) {
        if (transport != turnAllocation) {
            delete transport;
        }
    }
    transports.clear();

    // store candidates
    for (auto *socket : std::as_const(sockets)) {
        socket->setParent(q);

        auto *transport = new QXmppUdpTransport(socket, q);
        QObject::connect(transport, &QXmppIceTransport::datagramReceived,
                         q, &QXmppIceComponent::handleDatagram);

        QXmppJingleCandidate candidate = transport->localCandidate(component);

        transports << transport;
        localCandidates << candidate;
    }

    // start STUN checks
    // TODO: is this clear() enough
    stunTransactions.clear();
    for (auto &stunServer : config->stunServers) {
        QXmppStunMessage request;
        request.setType(int(QXmppStunMessage::Binding) | int(QXmppStunMessage::Request));
        for (auto *transport : std::as_const(transports)) {
            const QXmppJingleCandidate local = transport->localCandidate(component);
            if (!isCompatibleAddress(local.host(), stunServer.host)) {
                continue;
            }

            request.setId(QXmppUtils::generateRandomBytes(STUN_ID_SIZE));
            sendStunMessage(std::move(request), transport, stunServer);
        }
    }

    // connect to TURN server
    if (turnConfigured) {
        transports << turnAllocation;
        turnAllocation->connectToHost();
    }

    q->updateGatheringState();
}

void QXmppIceComponentPrivate::setTurnServer(const QHostAddress &host, quint16 port)
{
    turnAllocation->setServer(host, port);
    turnConfigured = !host.isNull() && port;
}

void QXmppIceComponentPrivate::setTurnUser(const QString &user)
{
    turnAllocation->setUser(user);
}

void QXmppIceComponentPrivate::setTurnPassword(const QString &password)
{
    turnAllocation->setPassword(password);
}

void QXmppIceComponentPrivate::sendStunMessage(const QXmppStunMessage &message, QXmppIceTransport *transport, const StunServer &stunServer)
{
    stunTransactions.push_back({
        new QXmppStunTransaction(
            message,
            [this, transport, stunServer](const auto &message) {
                transport->writeDatagram(message.encode(), stunServer.host, stunServer.port);
#ifdef QXMPP_DEBUG_STUN
                q->logSent(u"STUN packet to %1 port %2\n%3"_s.arg(stunServer.host.toString(), QString::number(stunServer.port), message.toString()));
#endif
            },
            [this, transport](QXmppStunTransaction *transaction) {
                // remove from running transactions
                auto result = std::ranges::remove(stunTransactions, transaction, [](auto &tp) { return std::get<0>(tp); });
                stunTransactions.erase(result.begin(), result.end());

                q->handleStunResponse(transport, transaction->response());
                q->updateGatheringState();
                transaction->deleteLater();
            },
            q),
        transport,
    });
}

void QXmppIceComponentPrivate::writeStun(const QXmppStunMessage &message, QXmppIceTransport *transport, const QHostAddress &address, quint16 port)
{
    const QString messagePassword = (message.type() & 0xFF00) ? config->localPassword : config->remotePassword;
    const QByteArray data = message.encode(messagePassword.toUtf8());
    transport->writeDatagram(data, address, port);
#ifdef QXMPP_DEBUG_STUN
    q->logSent(u"STUN packet to %1 port %2\n%3"_s.arg(address.toString(), QString::number(port), message.toString()));
#endif
}

/*! Constructs a new QXmppIceComponent. */
QXmppIceComponent::QXmppIceComponent(int component, QXmppIcePrivate *config, QObject *parent)
    : QXmppLoggable(parent),
      d(std::make_unique<QXmppIceComponentPrivate>(component, config, this))
{
    d->timer = new QTimer(this);
    d->timer->setInterval(500);
    connect(d->timer, &QTimer::timeout,
            this, &QXmppIceComponent::checkCandidates);

    d->turnAllocation = new QXmppTurnAllocation(this);
    connect(d->turnAllocation, &QXmppTurnAllocation::connected,
            this, &QXmppIceComponent::turnConnected);
    connect(d->turnAllocation, &QXmppIceTransport::datagramReceived,
            this, &QXmppIceComponent::handleDatagram);
    connect(d->turnAllocation, &QXmppTurnAllocation::disconnected,
            this, &QXmppIceComponent::updateGatheringState);

    // calculate peer-reflexive candidate priority
    // see RFC 5245 -  7.1.2.1. PRIORITY and USE-CANDIDATE
    QXmppJingleCandidate reflexive;
    reflexive.setComponent(d->component);
    reflexive.setType(QXmppJingleCandidate::PeerReflexiveType);
    d->peerReflexivePriority = candidatePriority(reflexive);

    setObjectName(u"STUN(%1)"_s.arg(QString::number(d->component)));
}

/*! Destroys the QXmppIceComponent. */
QXmppIceComponent::~QXmppIceComponent()
{
    qDeleteAll(d->pairs);
}

/*!
    Returns the component id for the current socket, e.g. 1 for RTP
    and 2 for RTCP.
*/
int QXmppIceComponent::component() const
{
    return d->component;
}

void QXmppIceComponent::checkCandidates()
{
    if (d->config->remoteUser.isEmpty()) {
        return;
    }
    debug(u"Checking remote candidates"_s);

    for (auto *pair : std::as_const(d->pairs)) {
        if (pair->state() == CandidatePair::WaitingState) {
            d->performCheck(pair, d->config->iceControlling);
            break;
        }
    }
}

/*! Stops ICE connectivity checks and closes the underlying sockets. */
void QXmppIceComponent::close()
{
    for (auto *transport : std::as_const(d->transports)) {
        transport->disconnectFromHost();
    }
    d->turnAllocation->disconnectFromHost();
    d->timer->stop();
    d->activePair = nullptr;
}

/*! Starts ICE connectivity checks. */
void QXmppIceComponent::connectToHost()
{
    if (d->activePair) {
        return;
    }

    checkCandidates();
    d->timer->start();
}

/*! Returns true if ICE negotiation completed, false otherwise. */
bool QXmppIceComponent::isConnected() const
{
    return d->activePair != nullptr;
}

/*! Returns the list of local candidates. */
QList<QXmppJingleCandidate> QXmppIceComponent::localCandidates() const
{
    return d->localCandidates;
}

void QXmppIceComponent::handleDatagram(const QByteArray &buffer, const QHostAddress &remoteHost, quint16 remotePort)
{
    auto *transport = qobject_cast<QXmppIceTransport *>(sender());
    if (!transport) {
        return;
    }

    // if this is not a STUN message, emit it
    quint32 messageCookie;
    QByteArray messageId;
    quint16 messageType = QXmppStunMessage::peekType(buffer, messageCookie, messageId);
    if (!messageType || messageCookie != STUN_MAGIC) {
        // use this as an opportunity to flag a potential pair
        for (auto *pair : std::as_const(d->pairs)) {
            if (pair->remote.host() == remoteHost &&
                pair->remote.port() == remotePort) {
                d->fallbackPair = pair;
                break;
            }
        }
        Q_EMIT datagramReceived(buffer);
        return;
    }

    // check if it's STUN
    QXmppStunTransaction *stunTransaction = nullptr;
    for (const auto &[transaction, stunTransport] : std::as_const(d->stunTransactions)) {
        if (transaction->request().id() == messageId && stunTransport == transport) {
            stunTransaction = transaction;
            break;
        }
    }

    // determine password to use
    QString messagePassword;
    if (!stunTransaction) {
        messagePassword = (messageType & 0xFF00) ? d->config->remotePassword : d->config->localPassword;
        if (messagePassword.isEmpty()) {
            return;
        }
    }

    // parse STUN message
    QXmppStunMessage message;
    QStringList errors;
    if (!message.decode(buffer, messagePassword.toUtf8(), &errors)) {
        for (const auto &error : std::as_const(errors)) {
            warning(error);
        }
        return;
    }
#ifdef QXMPP_DEBUG_STUN
    logReceived(u"STUN packet from %1 port %2\n%3"_s.arg(remoteHost.toString(), QString::number(remotePort), message.toString()));
#endif

    // we only want binding requests and responses
    if (message.messageMethod() != QXmppStunMessage::Binding) {
        return;
    }

    // STUN checks
    if (stunTransaction) {
        stunTransaction->readStun(message);
        return;
    }

    // process message from peer
    CandidatePair *pair = nullptr;
    if (message.messageClass() == QXmppStunMessage::Request) {
        // check for role conflict
        if (d->config->iceControlling && (!message.iceControlling.isEmpty() || message.useCandidate)) {
            warning(u"Role conflict, expected to be controlling"_s);
            return;
        } else if (!d->config->iceControlling && !message.iceControlled.isEmpty()) {
            warning(u"Role conflict, expected to be controlled"_s);
            return;
        }

        // send a binding response
        QXmppStunMessage response;
        response.setId(message.id());
        response.setType(int(QXmppStunMessage::Binding) | int(QXmppStunMessage::Response));
        response.xorMappedHost = remoteHost;
        response.xorMappedPort = remotePort;
        d->writeStun(response, transport, remoteHost, remotePort);

        // find or create remote candidate
        QXmppJingleCandidate remoteCandidate;
        bool remoteCandidateFound = false;
        for (const auto &c : std::as_const(d->remoteCandidates)) {
            if (c.host() == remoteHost && c.port() == remotePort) {
                remoteCandidate = c;
                remoteCandidateFound = true;
                break;
            }
        }
        if (!remoteCandidateFound) {
            // 7.2.1.3. Learning Peer Reflexive Candidates
            remoteCandidate.setComponent(d->component);
            remoteCandidate.setHost(remoteHost);
            remoteCandidate.setId(QXmppUtils::generateStanzaHash(10));
            remoteCandidate.setPort(remotePort);
            remoteCandidate.setPriority(message.priority());
            remoteCandidate.setProtocol(u"udp"_s);
            remoteCandidate.setType(QXmppJingleCandidate::PeerReflexiveType);
            remoteCandidate.setFoundation(QXmppUtils::generateStanzaHash(32));

            d->remoteCandidates << remoteCandidate;
        }

        // construct pair
        for (auto *ptr : std::as_const(d->pairs)) {
            if (ptr->transport == transport && ptr->remote.host() == remoteHost && ptr->remote.port() == remotePort) {
                pair = ptr;
                break;
            }
        }
        if (!pair) {
            pair = new CandidatePair(d->component, d->config->iceControlling, this);
            pair->remote = remoteCandidate;
            pair->transport = transport;
            d->pairs << pair;

            std::sort(d->pairs.begin(), d->pairs.end(), candidatePairPtrLessThan);
        }

        switch (pair->state()) {
        case CandidatePair::FrozenState:
        case CandidatePair::WaitingState:
        case CandidatePair::FailedState:
            // send a triggered connectivity test
            if (!d->config->remoteUser.isEmpty()) {
                d->performCheck(pair, pair->nominating || d->config->iceControlling || message.useCandidate);
            }
            break;
        case CandidatePair::InProgressState:
            // FIXME: force retransmit now
            pair->nominating = pair->nominating || message.useCandidate;
            break;
        case CandidatePair::SucceededState:
            if (message.useCandidate) {
                pair->nominated = true;
            }
            break;
        }

    } else if (message.messageClass() == QXmppStunMessage::Response || message.messageClass() == QXmppStunMessage::Error) {

        // find the pair for this transaction
        for (auto *ptr : std::as_const(d->pairs)) {
            if (ptr->transaction && ptr->transaction->request().id() == message.id()) {
                pair = ptr;
                break;
            }
        }
        if (!pair) {
            return;
        }

        // check remote host and port
        if (remoteHost != pair->remote.host() || remotePort != pair->remote.port()) {
            QXmppStunMessage error;
            error.setType(QXmppStunMessage::Error);
            error.errorPhrase = u"Received response from unexpected %1:%1"_s.arg(remoteHost.toString(), QString::number(remotePort));
            pair->transaction->readStun(error);
            return;
        }

        pair->transaction->readStun(message);
    }

    // signal completion
    if (pair && pair->nominated) {
        d->timer->stop();
        if (!d->activePair || pair->priority() > d->activePair->priority()) {
            info(u"ICE pair selected %1 (priority: %2)"_s.arg(pair->toString(), QString::number(pair->priority())));
            const bool wasConnected = (d->activePair != nullptr);
            d->activePair = pair;
            if (!wasConnected) {
                Q_EMIT connected();
            }
        }
    }
}

void QXmppIceComponent::handleStunResponse(QXmppIceTransport *transport, const QXmppStunMessage &response)
{
    if (response.messageClass() == QXmppStunMessage::Response) {
        // determine server-reflexive address
        QHostAddress reflexiveHost;
        quint16 reflexivePort = 0;
        if (!response.xorMappedHost.isNull() && response.xorMappedPort != 0) {
            reflexiveHost = response.xorMappedHost;
            reflexivePort = response.xorMappedPort;
        } else if (!response.mappedHost.isNull() && response.mappedPort != 0) {
            reflexiveHost = response.mappedHost;
            reflexivePort = response.mappedPort;
        } else {
            warning(u"STUN server did not provide a reflexive address"_s);
            return;
        }

        // check whether this candidates is already known
        for (const auto &candidate : std::as_const(d->localCandidates)) {
            if (candidate.host() == reflexiveHost &&
                candidate.port() == reflexivePort &&
                candidate.type() == QXmppJingleCandidate::ServerReflexiveType) {
                return;
            }
        }

        // add the new local candidate
        debug(u"Adding server-reflexive candidate %1 port %2"_s.arg(reflexiveHost.toString(), QString::number(reflexivePort)));
        QXmppJingleCandidate candidate;
        candidate.setComponent(d->component);
        candidate.setHost(reflexiveHost);
        candidate.setId(QXmppUtils::generateStanzaHash(10));
        candidate.setPort(reflexivePort);
        candidate.setProtocol(u"udp"_s);
        candidate.setType(QXmppJingleCandidate::ServerReflexiveType);
        candidate.setPriority(candidatePriority(candidate));
        candidate.setFoundation(computeFoundation(
            candidate.type(),
            candidate.protocol(),
            transport->localCandidate(d->component).host()));

        d->localCandidates << candidate;

        Q_EMIT localCandidatesChanged();
    } else {
        debug(u"STUN test failed (error %1)"_s.arg(response.errorPhrase));
    }
}

void QXmppIceComponent::turnConnected()
{
    const QXmppJingleCandidate candidate = d->turnAllocation->localCandidate(d->component);

    // add the new local candidate
    debug(u"Adding relayed candidate %1 port %2"_s.arg(candidate.host().toString(), QString::number(candidate.port())));
    d->localCandidates << candidate;

    Q_EMIT localCandidatesChanged();
    updateGatheringState();
}

static QList<QUdpSocket *> reservePort(const QList<QHostAddress> &addresses, quint16 port, QObject *parent)
{
    QList<QUdpSocket *> sockets;
    for (const auto &address : addresses) {
        auto *socket = new QUdpSocket(parent);
        socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
        sockets << socket;
        if (!socket->bind(address, port)) {
            qDeleteAll(sockets);
            sockets.clear();
            break;
        }
    }
    return sockets;
}

/*! Returns the list of local network addresses. */
QList<QHostAddress> QXmppIceComponent::discoverAddresses()
{
    QList<QHostAddress> addresses;
    const auto interfaces = QNetworkInterface::allInterfaces();
    for (const auto &interface : interfaces) {
        if (!(interface.flags() & QNetworkInterface::IsRunning) ||
            interface.flags() & QNetworkInterface::IsLoopBack) {
            continue;
        }

        const auto addressEntries = interface.addressEntries();
        for (const auto &entry : addressEntries) {
            QHostAddress ip = entry.ip();
            if ((ip.protocol() != QAbstractSocket::IPv4Protocol &&
                 ip.protocol() != QAbstractSocket::IPv6Protocol) ||
                entry.netmask().isNull()) {
                continue;
            }

            // FIXME: for some reason we can have loopback addresses
            // even if the interface does not have the loopback flag
            if (isLoopbackAddress(ip)) {
                continue;
            }

            // FIXME: for now skip IPv6 link-local addresses, seems to upset
            // clients such as empathy
            if (isIPv6LinkLocalAddress(ip)) {
                ip.setScopeId(interface.name());
                continue;
            }
            addresses << ip;
        }
    }
    return addresses;
}

/*!
    Tries to bind \a count UDP sockets on each of the given \a addresses.

    The port numbers are chosen so that they are consecutive, starting at
    an even port. This makes them suitable for RTP/RTCP sockets pairs.

    \a addresses is the network address on which to bind the sockets. \a count
    is the number of ports to reserve. \a parent is the parent object for the
    sockets.
*/
QList<QUdpSocket *> QXmppIceComponent::reservePorts(const QList<QHostAddress> &addresses, int count, QObject *parent)
{
    QList<QUdpSocket *> sockets;
    if (addresses.isEmpty() || !count) {
        return sockets;
    }

    const int expectedSize = addresses.size() * count;
    quint16 port = 49152;
    while (sockets.size() != expectedSize) {
        // reserve first port (even number)
        if (port % 2) {
            port++;
        }
        QList<QUdpSocket *> socketChunk;
        while (socketChunk.isEmpty() && port <= 65536 - count) {
            socketChunk = reservePort(addresses, port, parent);
            if (socketChunk.isEmpty()) {
                port += 2;
            }
        }
        if (socketChunk.isEmpty()) {
            return sockets;
        }

        // reserve other ports
        sockets << socketChunk;
        for (int i = 1; i < count; ++i) {
            socketChunk = reservePort(addresses, ++port, parent);
            if (socketChunk.isEmpty()) {
                break;
            }
            sockets << socketChunk;
        }

        // cleanup if we failed
        if (sockets.size() != expectedSize) {
            qDeleteAll(sockets);
            sockets.clear();
        }
    }
    return sockets;
}

/*!
    Sends a \a datagram to the remote party.
*/
qint64 QXmppIceComponent::sendDatagram(const QByteArray &datagram)
{
    CandidatePair *pair = d->activePair ? d->activePair : d->fallbackPair;
    if (!pair) {
        return -1;
    }
    return pair->transport->writeDatagram(datagram, pair->remote.host(), pair->remote.port());
}

void QXmppIceComponent::updateGatheringState()
{
    QXmppIceConnection::GatheringState newGatheringState;
    if (d->transports.isEmpty()) {
        newGatheringState = QXmppIceConnection::NewGatheringState;
    } else if (!d->stunTransactions.empty() || d->turnAllocation->state() == QXmppTurnAllocation::ConnectingState) {
        newGatheringState = QXmppIceConnection::BusyGatheringState;
    } else {
        newGatheringState = QXmppIceConnection::CompleteGatheringState;
    }

    if (newGatheringState != d->gatheringState) {
        d->gatheringState = newGatheringState;
        Q_EMIT gatheringStateChanged();
    }
}

class QXmppIceConnectionPrivate : public QXmppIcePrivate
{
public:
    QXmppIceConnectionPrivate();

    QMap<int, QXmppIceComponent *> components;
    QTimer *connectTimer;

    QXmppIceConnection::GatheringState gatheringState;

    QHostAddress turnHost;
    quint16 turnPort;
    QString turnUser;
    QString turnPassword;
};

QXmppIceConnectionPrivate::QXmppIceConnectionPrivate()
    : connectTimer(nullptr), gatheringState(QXmppIceConnection::NewGatheringState), turnPort(0)
{
}

/*!
    Constructs a new ICE connection with \a parent.
*/
QXmppIceConnection::QXmppIceConnection(QObject *parent)
    : QXmppLoggable(parent),
      d(std::make_unique<QXmppIceConnectionPrivate>())
{
    // timer to limit connection time to 30 seconds
    d->connectTimer = new QTimer(this);
    d->connectTimer->setInterval(30000);
    d->connectTimer->setSingleShot(true);
    connect(d->connectTimer, &QTimer::timeout,
            this, &QXmppIceConnection::slotTimeout);
}

QXmppIceConnection::~QXmppIceConnection() = default;

/*!
    Returns the given \a component of this ICE connection.
*/
QXmppIceComponent *QXmppIceConnection::component(int component)
{
    return d->components.value(component);
}

/*!
    Adds a \a component to this ICE connection, for instance 1 for RTP
    or 2 for RTCP.
*/
void QXmppIceConnection::addComponent(int component)
{
    if (d->components.contains(component)) {
        warning(u"Already have component %1"_s.arg(QString::number(component)));
        return;
    }

    auto *socket = new QXmppIceComponent(component, d.get(), this);
    socket->d->setTurnServer(d->turnHost, d->turnPort);
    socket->d->setTurnUser(d->turnUser);
    socket->d->setTurnPassword(d->turnPassword);

    connect(socket, &QXmppIceComponent::localCandidatesChanged,
            this, &QXmppIceConnection::localCandidatesChanged);

    connect(socket, &QXmppIceComponent::connected,
            this, &QXmppIceConnection::slotConnected);

    connect(socket, &QXmppIceComponent::gatheringStateChanged,
            this, &QXmppIceConnection::slotGatheringStateChanged);

    d->components[component] = socket;
}

/*!
    Adds a \a candidate for one of the remote components.
*/
void QXmppIceConnection::addRemoteCandidate(const QXmppJingleCandidate &candidate)
{
    QXmppIceComponent *socket = d->components.value(candidate.component());
    if (!socket) {
        warning(u"Not adding candidate for unknown component %1"_s.arg(QString::number(candidate.component())));
        return;
    }
    socket->d->addRemoteCandidate(candidate);
}

/*!
    Binds the local sockets to the specified \a addresses on which to listen. Returns true on success.
*/
bool QXmppIceConnection::bind(const QList<QHostAddress> &addresses)
{
    // reserve ports
    QList<QUdpSocket *> sockets = QXmppIceComponent::reservePorts(addresses, d->components.size());
    if (sockets.isEmpty() && !addresses.isEmpty()) {
        return false;
    }

    // assign sockets
    QList<int> keys = d->components.keys();
    std::sort(keys.begin(), keys.end());
    int s = 0;
    for (const auto k : std::as_const(keys)) {
        d->components[k]->d->setSockets(sockets.mid(s, addresses.size()));
        s += addresses.size();
    }

    return true;
}

/*! Closes the ICE connection. */
void QXmppIceConnection::close()
{
    d->connectTimer->stop();
    for (auto *socket : std::as_const(d->components)) {
        socket->close();
    }
}

/*! Starts ICE connectivity checks. */
void QXmppIceConnection::connectToHost()
{
    if (isConnected() || d->connectTimer->isActive()) {
        return;
    }

    for (auto *socket : std::as_const(d->components)) {
        socket->connectToHost();
    }
    d->connectTimer->start();
}

/*! Returns true if ICE negotiation completed, false otherwise. */
bool QXmppIceConnection::isConnected() const
{
    for (auto *socket : std::as_const(d->components)) {
        if (!socket->isConnected()) {
            return false;
        }
    }
    return true;
}

/*!
    Returns the ICE gathering state, that is the discovery of local
    candidates.

    \since QXmpp 0.9.3
*/
QXmppIceConnection::GatheringState QXmppIceConnection::gatheringState() const
{
    return d->gatheringState;
}

/*!
    Sets whether the local party has the ICE controlling role.

    note This must be called only once, immediately after creating
    the connection.

    \a controlling.
*/
void QXmppIceConnection::setIceControlling(bool controlling)
{
    d->iceControlling = controlling;
}

/*!
    Returns the list of local HOST CANDIDATES candidates by iterating
    over the available network interfaces.
*/
QList<QXmppJingleCandidate> QXmppIceConnection::localCandidates() const
{
    QList<QXmppJingleCandidate> candidates;
    for (auto *socket : std::as_const(d->components)) {
        candidates += socket->localCandidates();
    }
    return candidates;
}

/*! Returns the local user fragment. */
QString QXmppIceConnection::localUser() const
{
    return d->localUser;
}

/*! Returns the local password. */
QString QXmppIceConnection::localPassword() const
{
    return d->localPassword;
}

/*!
    Sets the remote \a user fragment.
*/
void QXmppIceConnection::setRemoteUser(const QString &user)
{
    d->remoteUser = user;
}

/*!
    Sets the remote \a password.
*/
void QXmppIceConnection::setRemotePassword(const QString &password)
{
    d->remotePassword = password;
}

/*!
    Sets multiple STUN \a servers to use to determine server-reflexive
    addresses and ports.

    \note This may only be called prior to calling bind().

    \since QXmpp 1.14
*/
void QXmppIceConnection::setStunServers(const QList<QXmpp::StunServer> &servers)
{
    d->stunServers = servers;
}

/*!
    Sets the TURN server to use to relay packets in double-NAT configurations.

    \note This may only be called prior to calling bind().

    \since QXmpp 1.14

    \a server.
*/
void QXmppIceConnection::setTurnServer(const TurnServer &server)
{
    d->turnHost = server.host;
    d->turnPort = server.port;
    d->turnUser = server.username;
    d->turnPassword = server.password;

    for (auto *socket : std::as_const(d->components)) {
        socket->d->setTurnServer(server.host, server.port);
        socket->d->setTurnUser(server.username);
        socket->d->setTurnPassword(server.password);
    }
}

/*!
    Sets multiple STUN \a servers to use to determine server-reflexive
    addresses and ports.

    \note This may only be called prior to calling bind().

    \deprecated Use setStunServers(const QList<QXmpp::StunServer> &)

    \since QXmpp 1.3
*/
void QXmppIceConnection::setStunServers(const QList<QPair<QHostAddress, quint16>> &servers)
{
    d->stunServers = transform<QList<StunServer>>(servers, [](const auto &s) { return StunServer(s.first, s.second); });
}

/*!
    Sets a single STUN server to use to determine server-reflexive addresses
    and ports.

    \note This may only be called prior to calling bind().

    \a host is the address of the STUN server. \a port is the port of the STUN
    server.

    \deprecated Use setStunServers(const QList<QXmpp::StunServer> &)
*/
void QXmppIceConnection::setStunServer(const QHostAddress &host, quint16 port)
{
    d->stunServers.clear();
    d->stunServers.push_back({ host, port });
}

/*!
    Sets the TURN server to use to relay packets in double-NAT configurations.

    \note This may only be called prior to calling bind().

    \a host is the address of the TURN server. \a port is the port of the TURN
    server.

    \deprecated Use setTurnServer(const QXmpp::TurnServer &)
*/
void QXmppIceConnection::setTurnServer(const QHostAddress &host, quint16 port)
{
    d->turnHost = host;
    d->turnPort = port;
    for (auto *socket : std::as_const(d->components)) {
        socket->d->setTurnServer(host, port);
    }
}

/*!
    Sets the \a user used for authentication with the TURN server.

    \note This may only be called prior to calling bind().

    \deprecated Use setTurnServer(const QXmpp::TurnServer &)
*/
void QXmppIceConnection::setTurnUser(const QString &user)
{
    d->turnUser = user;
    for (auto *socket : std::as_const(d->components)) {
        socket->d->setTurnUser(user);
    }
}

/*!
    Sets the \a password used for authentication with the TURN server.

    \note This may only be called prior to calling bind().

    \deprecated Use setTurnServer(const QXmpp::TurnServer &)
*/
void QXmppIceConnection::setTurnPassword(const QString &password)
{
    d->turnPassword = password;
    for (auto *socket : std::as_const(d->components)) {
        socket->d->setTurnPassword(password);
    }
}

void QXmppIceConnection::slotConnected()
{
    for (auto *socket : std::as_const(d->components)) {
        if (!socket->isConnected()) {
            return;
        }
    }
    info(u"ICE negotiation completed"_s);
    d->connectTimer->stop();
    Q_EMIT connected();
}

void QXmppIceConnection::slotGatheringStateChanged()
{
    GatheringState newGatheringState;
    bool allComplete = true;
    bool allNew = true;
    for (auto *socket : std::as_const(d->components)) {
        if (socket->d->gatheringState != CompleteGatheringState) {
            allComplete = false;
        }
        if (socket->d->gatheringState != NewGatheringState) {
            allNew = false;
        }
    }
    if (allNew) {
        newGatheringState = NewGatheringState;
    } else if (allComplete) {
        newGatheringState = CompleteGatheringState;
    } else {
        newGatheringState = BusyGatheringState;
    }

    if (newGatheringState != d->gatheringState) {
        info(u"ICE gathering state changed from '%1' to '%2'"_s
                 .arg(Enums::toString(d->gatheringState), Enums::toString(newGatheringState)));
        d->gatheringState = newGatheringState;
        Q_EMIT gatheringStateChanged();
    }
}

void QXmppIceConnection::slotTimeout()
{
    warning(u"ICE negotiation timed out"_s);
    for (auto *socket : std::as_const(d->components)) {
        socket->close();
    }
    Q_EMIT disconnected();
}

QXmppIceTransport::QXmppIceTransport(QObject *parent)
    : QXmppLoggable(parent)
{
}

QXmppIceTransport::~QXmppIceTransport()
{
}
