diff options
| author | Jeremy Lainé <jeremy.laine@m4x.org> | 2012-02-08 12:51:15 +0000 |
|---|---|---|
| committer | Jeremy Lainé <jeremy.laine@m4x.org> | 2012-02-08 12:51:15 +0000 |
| commit | deb9d6cb53057ca8b90d10d6a3bdc5dcfd1b3ee4 (patch) | |
| tree | d956bad28e28aadc3c83dbf88b3eddb5e1d9a9f4 /src/base/QXmppStun.cpp | |
| parent | e8a1ad0cc608f12874ba4bafbd8282fa537ec9fb (diff) | |
| download | qxmpp-deb9d6cb53057ca8b90d10d6a3bdc5dcfd1b3ee4.tar.gz | |
move files common to client/server into "base"
Diffstat (limited to 'src/base/QXmppStun.cpp')
| -rw-r--r-- | src/base/QXmppStun.cpp | 2581 |
1 files changed, 2581 insertions, 0 deletions
diff --git a/src/base/QXmppStun.cpp b/src/base/QXmppStun.cpp new file mode 100644 index 00000000..16cef928 --- /dev/null +++ b/src/base/QXmppStun.cpp @@ -0,0 +1,2581 @@ +/* + * Copyright (C) 2008-2011 The QXmpp developers + * + * Author: + * Jeremy Lainé + * + * Source: + * http://code.google.com/p/qxmpp + * + * This file is a part of QXmpp library. + * + * 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. + * + */ + +#define QXMPP_DEBUG_STUN + +#include <QCryptographicHash> +#include <QHostInfo> +#include <QNetworkInterface> +#include <QUdpSocket> +#include <QTimer> + +#include "QXmppStun.h" +#include "QXmppUtils.h" + +#define ID_SIZE 12 +#define STUN_RTO_INTERVAL 500 +#define STUN_RTO_MAX 7 + +static const quint32 STUN_MAGIC = 0x2112A442; +static const quint16 STUN_HEADER = 20; +static const quint8 STUN_IPV4 = 0x01; +static const quint8 STUN_IPV6 = 0x02; + +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 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 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() + : errorCode(0), + changedPort(0), + mappedPort(0), + otherPort(0), + sourcePort(0), + xorMappedPort(0), + xorPeerPort(0), + xorRelayedPort(0), + useCandidate(false), + m_cookie(STUN_MAGIC), + m_type(0), + m_changeRequest(0), + m_channelNumber(0), + m_lifetime(0), + m_priority(0) +{ + m_id = QByteArray(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() == 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, indicating whether to change +/// the IP and / or port from which the response is sent. +/// +/// \param changeRequest + +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. +/// +/// \param 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, indicating the duration in seconds for +/// which the server will maintain an allocation. +/// +/// \param lifetime + +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. +/// +/// \param 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, the priority that would be assigned to +/// a peer reflexive candidate discovered during the ICE check. +/// +/// \param priority + +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. +/// +/// \param 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. +/// +/// \param 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. +/// +/// \param 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, containing a textual description of the +/// software being used. +/// +/// \param software + +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, containing the username to use for +/// authentication. +/// +/// \param username + +void QXmppStunMessage::setUsername(const QString &username) +{ + m_username = username; + m_attributes << Username; +} + +/// Decodes a QXmppStunMessage and checks its integrity using the given key. +/// +/// \param buffer +/// \param key +/// \param errors + +bool QXmppStunMessage::decode(const QByteArray &buffer, const QByteArray &key, QStringList *errors) +{ + QStringList silent; + if (!errors) + errors = &silent; + + if (buffer.size() < STUN_HEADER) + { + *errors << QLatin1String("Received a truncated STUN packet"); + 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 << QLatin1String("Received an invalid STUN packet"); + 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 << QString("Skipping attribute %1 after MESSAGE-INTEGRITY").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 << QLatin1String("Bad MAPPED-ADDRESS"); + 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 << QLatin1String("Bad SOURCE-ADDRESS"); + return false; + } + + } else if (a_type == ChangedAddress) { + + // CHANGED-ADDRESS + if (!decodeAddress(stream, a_length, changedHost, changedPort)) + { + *errors << QLatin1String("Bad CHANGED-ADDRESS"); + return false; + } + + } else if (a_type == OtherAddress) { + + // OTHER-ADDRESS + if (!decodeAddress(stream, a_length, otherHost, otherPort)) + { + *errors << QLatin1String("Bad OTHER-ADDRESS"); + return false; + } + + } else if (a_type == XorMappedAddress) { + + // XOR-MAPPED-ADDRESS + if (!decodeAddress(stream, a_length, xorMappedHost, xorMappedPort, m_id)) + { + *errors << QLatin1String("Bad XOR-MAPPED-ADDRESS"); + return false; + } + + } else if (a_type == XorPeerAddress) { + + // XOR-PEER-ADDRESS + if (!decodeAddress(stream, a_length, xorPeerHost, xorPeerPort, m_id)) + { + *errors << QLatin1String("Bad XOR-PEER-ADDRESS"); + return false; + } + + } else if (a_type == XorRelayedAddress) { + + // XOR-RELAYED-ADDRESS + if (!decodeAddress(stream, a_length, xorRelayedHost, xorRelayedPort, m_id)) + { + *errors << QLatin1String("Bad XOR-RELAYED-ADDRESS"); + 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 != generateHmacSha1(key, copy)) + { + *errors << QLatin1String("Bad message integrity"); + 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 = generateCrc32(copy) ^ 0x5354554eL; + if (fingerprint != expected) + { + *errors << QLatin1String("Bad fingerprint"); + 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 << QString("Skipping unknown attribute %1").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 key. +/// +/// \param key +/// \param addFingerprint + +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()); + } + + // 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 = 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 = generateCrc32(buffer) ^ 0x5354554eL; + stream << quint16(Fingerprint); + stream << quint16(sizeof(fingerprint)); + stream << fingerprint; + } + + return buffer; +} + +/// If the given packet looks like a STUN message, returns the message +/// type, otherwise returns 0. +/// +/// \param buffer +/// \param cookie +/// \param id + +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(ID_SIZE); + stream.readRawData(id.data(), id.size()); + return type; +} + +QString QXmppStunMessage::toString() const +{ + QStringList dumpLines; + QString typeName; + switch (messageMethod()) + { + case Binding: typeName = "Binding"; break; + case SharedSecret: typeName = "Shared Secret"; break; + case Allocate: typeName = "Allocate"; break; + case Refresh: typeName = "Refresh"; break; + case Send: typeName = "Send"; break; + case Data: typeName = "Data"; break; + case CreatePermission: typeName = "CreatePermission"; break; + case ChannelBind: typeName = "ChannelBind"; break; + default: typeName = "Unknown"; break; + } + switch (messageClass()) + { + case Request: typeName += " Request"; break; + case Indication: typeName += " Indication"; break; + case Response: typeName += " Response"; break; + case Error: typeName += " Error"; break; + default: break; + } + dumpLines << QString(" type %1 (%2)") + .arg(typeName) + .arg(QString::number(m_type)); + dumpLines << QString(" id %1").arg(QString::fromAscii(m_id.toHex())); + + // attributes + if (m_attributes.contains(ChannelNumber)) + dumpLines << QString(" * CHANNEL-NUMBER %1").arg(QString::number(m_channelNumber)); + if (errorCode) + dumpLines << QString(" * ERROR-CODE %1 %2") + .arg(QString::number(errorCode), errorPhrase); + if (m_attributes.contains(Lifetime)) + dumpLines << QString(" * LIFETIME %1").arg(QString::number(m_lifetime)); + if (m_attributes.contains(Nonce)) + dumpLines << QString(" * NONCE %1").arg(QString::fromLatin1(m_nonce)); + if (m_attributes.contains(Realm)) + dumpLines << QString(" * REALM %1").arg(m_realm); + if (m_attributes.contains(RequestedTransport)) + dumpLines << QString(" * REQUESTED-TRANSPORT 0x%1").arg(QString::number(m_requestedTransport, 16)); + if (m_attributes.contains(ReservationToken)) + dumpLines << QString(" * RESERVATION-TOKEN %1").arg(QString::fromAscii(m_reservationToken.toHex())); + if (m_attributes.contains(Software)) + dumpLines << QString(" * SOFTWARE %1").arg(m_software); + if (m_attributes.contains(Username)) + dumpLines << QString(" * USERNAME %1").arg(m_username); + if (mappedPort) + dumpLines << QString(" * MAPPED-ADDRESS %1 %2") + .arg(mappedHost.toString(), QString::number(mappedPort)); + if (m_attributes.contains(ChangeRequest)) + dumpLines << QString(" * CHANGE-REQUEST %1") + .arg(QString::number(m_changeRequest)); + if (sourcePort) + dumpLines << QString(" * SOURCE-ADDRESS %1 %2") + .arg(sourceHost.toString(), QString::number(sourcePort)); + if (changedPort) + dumpLines << QString(" * CHANGED-ADDRESS %1 %2") + .arg(changedHost.toString(), QString::number(changedPort)); + if (otherPort) + dumpLines << QString(" * OTHER-ADDRESS %1 %2") + .arg(otherHost.toString(), QString::number(otherPort)); + if (xorMappedPort) + dumpLines << QString(" * XOR-MAPPED-ADDRESS %1 %2") + .arg(xorMappedHost.toString(), QString::number(xorMappedPort)); + if (xorPeerPort) + dumpLines << QString(" * XOR-PEER-ADDRESS %1 %2") + .arg(xorPeerHost.toString(), QString::number(xorPeerPort)); + if (xorRelayedPort) + dumpLines << QString(" * XOR-RELAYED-ADDRESS %1 %2") + .arg(xorRelayedHost.toString(), QString::number(xorRelayedPort)); + if (m_attributes.contains(Priority)) + dumpLines << QString(" * PRIORITY %1").arg(QString::number(m_priority)); + if (!iceControlling.isEmpty()) + dumpLines << QString(" * ICE-CONTROLLING %1") + .arg(QString::fromAscii(iceControlling.toHex())); + if (!iceControlled.isEmpty()) + dumpLines << QString(" * ICE-CONTROLLED %1") + .arg(QString::fromAscii(iceControlled.toHex())); + + return dumpLines.join("\n"); +} + +/// Constructs a new QXmppStunTransaction. +/// +/// \param request +/// \param receiver + +QXmppStunTransaction::QXmppStunTransaction(const QXmppStunMessage &request, QObject *receiver) + : QXmppLoggable(receiver), + m_request(request), + m_tries(0) +{ + bool check; + Q_UNUSED(check); + + check = connect(this, SIGNAL(writeStun(QXmppStunMessage)), + receiver, SLOT(writeStun(QXmppStunMessage))); + Q_ASSERT(check); + + check = connect(this, SIGNAL(finished()), + receiver, SLOT(transactionFinished())); + Q_ASSERT(check); + + // RTO timer + m_retryTimer = new QTimer(this); + m_retryTimer->setSingleShot(true); + check = connect(m_retryTimer, SIGNAL(timeout()), + this, SLOT(retry())); + + // send packet immediately + m_tries++; + emit writeStun(m_request); + m_retryTimer->start(STUN_RTO_INTERVAL); +} + +void QXmppStunTransaction::readStun(const QXmppStunMessage &response) +{ + if (response.messageClass() == QXmppStunMessage::Error || + response.messageClass() == QXmppStunMessage::Response) { + m_response = response; + emit finished(); + } +} + +/// Returns the STUN request. + +QXmppStunMessage QXmppStunTransaction::request() const +{ + return m_request; +} + +/// Returns the STUN response. + +QXmppStunMessage QXmppStunTransaction::response() const +{ + return m_response; +} + +void QXmppStunTransaction::retry() +{ + if (m_tries >= STUN_RTO_MAX) { + m_response.setType(QXmppStunMessage::Error); + m_response.errorPhrase = QLatin1String("Request timed out"); + emit finished(); + return; + } + + // resend request + m_tries++; + emit writeStun(m_request); + m_retryTimer->start(2 * m_retryTimer->interval()); +} + +/// Constructs a new QXmppTurnAllocation. +/// +/// \param parent + +QXmppTurnAllocation::QXmppTurnAllocation(QObject *parent) + : QXmppLoggable(parent), + m_relayedPort(0), + m_turnPort(0), + m_channelNumber(0x4000), + m_lifetime(600), + m_state(UnconnectedState) +{ + bool check; + Q_UNUSED(check); + + socket = new QUdpSocket(this); + check = connect(socket, SIGNAL(readyRead()), + this, SLOT(readyRead())); + Q_ASSERT(check); + + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + check = connect(m_timer, SIGNAL(timeout()), + this, SLOT(refresh())); + Q_ASSERT(check); + + // channels are valid 600s, we refresh every 500s + m_channelTimer = new QTimer(this); + m_channelTimer->setInterval(500 * 1000); + check = connect(m_channelTimer, SIGNAL(timeout()), + this, SLOT(refreshChannels())); + Q_ASSERT(check); +} + +/// 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("Could not start listening for TURN"); + return; + } + } + + // send allocate request + QXmppStunMessage request; + request.setType(QXmppStunMessage::Allocate | QXmppStunMessage::Request); + request.setId(generateRandomBytes(12)); + request.setLifetime(m_lifetime); + request.setRequestedTransport(0x11); + m_transactions << new QXmppStunTransaction(request, this); + + // 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(); + foreach (QXmppStunTransaction *transaction, m_transactions) + delete transaction; + m_transactions.clear(); + + // end allocation + if (m_state == ConnectedState) { + QXmppStunMessage request; + request.setType(QXmppStunMessage::Refresh | QXmppStunMessage::Request); + request.setId(generateRandomBytes(12)); + request.setNonce(m_nonce); + request.setRealm(m_realm); + request.setUsername(m_username); + request.setLifetime(0); + m_transactions << new QXmppStunTransaction(request, this); + + setState(ClosingState); + } else { + setState(UnconnectedState); + } +} + +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) { + 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)) { + foreach (const QString &error, errors) + warning(error); + return; + } + +#ifdef QXMPP_DEBUG_STUN + logReceived(QString("TURN packet from %1 port %2\n%3").arg( + remoteHost.toString(), + QString::number(remotePort), + message.toString())); +#endif + + // find transaction + foreach (QXmppStunTransaction *transaction, m_transactions) { + if (transaction->request().id() == message.id() && + transaction->request().messageMethod() == message.messageMethod()) { + transaction->readStun(message); + return; + } + } +} + +/// Refresh allocation. + +void QXmppTurnAllocation::refresh() +{ + QXmppStunMessage request; + request.setType(QXmppStunMessage::Refresh | QXmppStunMessage::Request); + request.setId(generateRandomBytes(12)); + request.setNonce(m_nonce); + request.setRealm(m_realm); + request.setUsername(m_username); + m_transactions << new QXmppStunTransaction(request, this); +} + +/// Refresh channel bindings. + +void QXmppTurnAllocation::refreshChannels() +{ + foreach (quint16 channel, m_channels.keys()) { + QXmppStunMessage request; + request.setType(QXmppStunMessage::ChannelBind | QXmppStunMessage::Request); + request.setId(generateRandomBytes(12)); + request.setNonce(m_nonce); + request.setRealm(m_realm); + request.setUsername(m_username); + request.setChannelNumber(channel); + request.xorPeerHost = m_channels[channel].first; + request.xorPeerPort = m_channels[channel].second; + m_transactions << new QXmppStunTransaction(request, this); + } +} + +/// 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 password used to authenticate with the TURN server. +/// +/// \param password + +void QXmppTurnAllocation::setPassword(const QString &password) +{ + m_password = password; +} + +/// Sets the TURN server to use. +/// +/// \param host The address of the TURN server. +/// \param port 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. +/// +/// \param user + +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) { + emit connected(); + } else if (m_state == UnconnectedState) { + m_timer->stop(); + emit disconnected(); + } +} + +void QXmppTurnAllocation::transactionFinished() +{ + QXmppStunTransaction *transaction = qobject_cast<QXmppStunTransaction*>(sender()); + if (!transaction || !m_transactions.removeAll(transaction)) + return; + transaction->deleteLater(); + + // 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 + ":" + m_realm + ":" + m_password).toUtf8()); + m_key = hash.result(); + + // retry request + QXmppStunMessage request(transaction->request()); + request.setId(generateRandomBytes(12)); + request.setNonce(m_nonce); + request.setRealm(m_realm); + request.setUsername(m_username); + m_transactions << new QXmppStunTransaction(request, this); + return; + } + + const quint16 method = transaction->request().messageMethod(); + if (method == QXmppStunMessage::Allocate) { + + if (reply.messageClass() == QXmppStunMessage::Error) { + warning(QString("Allocation failed: %1 %2").arg( + QString::number(reply.errorCode), reply.errorPhrase)); + setState(UnconnectedState); + return; + } + if (reply.xorRelayedHost.isNull() || + reply.xorRelayedHost.protocol() != QAbstractSocket::IPv4Protocol || + !reply.xorRelayedPort) { + warning("Allocation did not yield a valid relayed address"); + 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(QString("ChannelBind failed: %1 %2").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(QString("Refresh failed: %1 %2").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(QXmppStunMessage::ChannelBind | QXmppStunMessage::Request); + request.setId(generateRandomBytes(12)); + request.setNonce(m_nonce); + request.setRealm(m_realm); + request.setUsername(m_username); + request.setChannelNumber(channel); + request.xorPeerHost = host; + request.xorPeerPort = port; + m_transactions << new QXmppStunTransaction(request, this); + + // 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(QString("TURN packet to %1 port %2\n%3").arg( + m_turnHost.toString(), + QString::number(m_turnPort), + message.toString())); +#endif +} + +QXmppIceComponent::Pair::Pair(int component, bool controlling) + : checked(QIODevice::NotOpen), + socket(0), + m_component(component), + m_controlling(controlling) +{ + transaction = generateRandomBytes(ID_SIZE); +} + +quint64 QXmppIceComponent::Pair::priority() const +{ + QXmppJingleCandidate local; + local.setComponent(m_component); + local.setType(socket ? QXmppJingleCandidate::HostType : QXmppJingleCandidate::RelayedType); + local.setPriority(candidatePriority(local)); + + // 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); +} + +QString QXmppIceComponent::Pair::toString() const +{ + QString str = QString("%1 port %2").arg(remote.host().toString(), QString::number(remote.port())); + if (socket) + str += QString(" (local %1 port %2)").arg(socket->localAddress().toString(), QString::number(socket->localPort())); + else + str += QString(" (relayed)"); + if (!reflexive.host().isNull() && reflexive.port()) + str += QString(" (reflexive %1 port %2)").arg(reflexive.host().toString(), QString::number(reflexive.port())); + return str; +} + +/// Constructs a new QXmppIceComponent. +/// +/// \param controlling +/// \param parent + +QXmppIceComponent::QXmppIceComponent(QObject *parent) + : QXmppLoggable(parent), + m_component(0), + m_activePair(0), + m_fallbackPair(0), + m_iceControlling(false), + m_peerReflexivePriority(0), + m_stunPort(0), + m_stunTries(0), + m_turnConfigured(false) +{ + bool check; + Q_UNUSED(check); + + m_localUser = generateStanzaHash(4); + m_localPassword = generateStanzaHash(22); + + m_timer = new QTimer(this); + m_timer->setInterval(500); + check = connect(m_timer, SIGNAL(timeout()), + this, SLOT(checkCandidates())); + Q_ASSERT(check); + + m_stunTimer = new QTimer(this); + m_stunTimer->setInterval(500); + check = connect(m_stunTimer, SIGNAL(timeout()), + this, SLOT(checkStun())); + Q_ASSERT(check); + + m_turnAllocation = new QXmppTurnAllocation(this); + check = connect(m_turnAllocation, SIGNAL(connected()), + this, SLOT(turnConnected())); + Q_ASSERT(check); + check = connect(m_turnAllocation, SIGNAL(datagramReceived(QByteArray,QHostAddress,quint16)), + this, SLOT(handleDatagram(QByteArray,QHostAddress,quint16))); + Q_ASSERT(check); +} + +/// Destroys the QXmppIceComponent. + +QXmppIceComponent::~QXmppIceComponent() +{ + foreach (Pair *pair, m_pairs) + delete pair; +} + +/// Returns the component id for the current socket, e.g. 1 for RTP +/// and 2 for RTCP. + +int QXmppIceComponent::component() const +{ + return m_component; +} + +/// Sets the component id for the current socket, e.g. 1 for RTP +/// and 2 for RTCP. +/// +/// \param component + +void QXmppIceComponent::setComponent(int component) +{ + m_component = component; + + // calculate peer-reflexive candidate priority + // see RFC 5245 - 7.1.2.1. PRIORITY and USE-CANDIDATE + QXmppJingleCandidate reflexive; + reflexive.setComponent(m_component); + reflexive.setType(QXmppJingleCandidate::PeerReflexiveType); + m_peerReflexivePriority = candidatePriority(reflexive); + + setObjectName(QString("STUN(%1)").arg(QString::number(m_component))); +} + +void QXmppIceComponent::checkCandidates() +{ + debug("Checking remote candidates"); + foreach (Pair *pair, m_pairs) + { + if (m_remoteUser.isEmpty()) + continue; + + // send a binding request + QXmppStunMessage message; + message.setId(pair->transaction); + message.setType(QXmppStunMessage::Binding | QXmppStunMessage::Request); + message.setPriority(m_peerReflexivePriority); + message.setUsername(QString("%1:%2").arg(m_remoteUser, m_localUser)); + if (m_iceControlling) + { + message.iceControlling = QByteArray(8, 0); + message.useCandidate = true; + } else { + message.iceControlled = QByteArray(8, 0); + } + writeStun(message, pair); + } + +} + +void QXmppIceComponent::checkStun() +{ + if (m_stunHost.isNull() || !m_stunPort || m_stunTries > 10) { + m_stunTimer->stop(); + return; + } + + // Send a request to STUN server to determine server-reflexive candidate + foreach (QUdpSocket *socket, m_sockets) + { + QXmppStunMessage msg; + msg.setType(QXmppStunMessage::Binding | QXmppStunMessage::Request); + msg.setId(m_stunId); +#ifdef QXMPP_DEBUG_STUN + logSent(QString("STUN packet to %1 port %2\n%3").arg(m_stunHost.toString(), + QString::number(m_stunPort), msg.toString())); +#endif + socket->writeDatagram(msg.encode(), m_stunHost, m_stunPort); + } + m_stunTries++; +} + +/// Stops ICE connectivity checks and closes the underlying sockets. + +void QXmppIceComponent::close() +{ + foreach (QUdpSocket *socket, m_sockets) + socket->close(); + m_turnAllocation->disconnectFromHost(); + m_timer->stop(); + m_stunTimer->stop(); + m_activePair = 0; +} + +/// Starts ICE connectivity checks. + +void QXmppIceComponent::connectToHost() +{ + if (m_activePair) + return; + + checkCandidates(); + m_timer->start(); +} + +/// Returns true if ICE negotiation completed, false otherwise. + +bool QXmppIceComponent::isConnected() const +{ + return m_activePair != 0; +} + +void QXmppIceComponent::setIceControlling(bool controlling) +{ + m_iceControlling = controlling; +} + +/// Returns the list of local candidates. + +QList<QXmppJingleCandidate> QXmppIceComponent::localCandidates() const +{ + return m_localCandidates; +} + +/// Sets the local user fragment. +/// +/// \param user + +void QXmppIceComponent::setLocalUser(const QString &user) +{ + m_localUser = user; +} + +/// Sets the local password. +/// +/// \param password + +void QXmppIceComponent::setLocalPassword(const QString &password) +{ + m_localPassword = password; +} + +/// Adds a remote STUN candidate. + +bool QXmppIceComponent::addRemoteCandidate(const QXmppJingleCandidate &candidate) +{ + if (candidate.component() != m_component || + (candidate.type() != QXmppJingleCandidate::HostType && + candidate.type() != QXmppJingleCandidate::RelayedType && + candidate.type() != QXmppJingleCandidate::ServerReflexiveType) || + candidate.protocol() != "udp" || + (candidate.host().protocol() != QAbstractSocket::IPv4Protocol && + candidate.host().protocol() != QAbstractSocket::IPv6Protocol)) + return false; + + foreach (Pair *pair, m_pairs) + if (pair->remote.host() == candidate.host() && + pair->remote.port() == candidate.port()) + return false; + + foreach (QUdpSocket *socket, m_sockets) + { + // do not pair IPv4 with IPv6 or global with link-local addresses + if (socket->localAddress().protocol() != candidate.host().protocol() || + isIPv6LinkLocalAddress(socket->localAddress()) != isIPv6LinkLocalAddress(candidate.host())) + continue; + + Pair *pair = new Pair(m_component, m_iceControlling); + pair->remote = candidate; + if (isIPv6LinkLocalAddress(pair->remote.host())) + { + QHostAddress remoteHost = pair->remote.host(); + remoteHost.setScopeId(socket->localAddress().scopeId()); + pair->remote.setHost(remoteHost); + } + pair->socket = socket; + m_pairs << pair; + + if (!m_fallbackPair) + m_fallbackPair = pair; + } + + // only use relaying for IPv4 candidates + if (m_turnConfigured && candidate.host().protocol() == QAbstractSocket::IPv4Protocol) { + Pair *pair = new Pair(m_component, m_iceControlling); + pair->remote = candidate; + pair->socket = 0; + m_pairs << pair; + } + return true; +} + +/// Adds a discovered peer-reflexive STUN candidate. + +QXmppIceComponent::Pair *QXmppIceComponent::addRemoteCandidate(QUdpSocket *socket, const QHostAddress &host, quint16 port, quint32 priority) +{ + foreach (Pair *pair, m_pairs) + if (pair->remote.host() == host && + pair->remote.port() == port && + pair->socket == socket) + return pair; + + QXmppJingleCandidate candidate; + candidate.setComponent(m_component); + candidate.setHost(host); + candidate.setId(generateStanzaHash(10)); + candidate.setPort(port); + candidate.setPriority(priority); + candidate.setProtocol("udp"); + candidate.setType(QXmppJingleCandidate::PeerReflexiveType); + + Pair *pair = new Pair(m_component, m_iceControlling); + pair->remote = candidate; + pair->socket = socket; + m_pairs << pair; + + debug(QString("Added candidate %1").arg(pair->toString())); + return pair; +} + +/// Sets the remote user fragment. +/// +/// \param user + +void QXmppIceComponent::setRemoteUser(const QString &user) +{ + m_remoteUser = user; +} + +/// Sets the remote password. +/// +/// \param password + +void QXmppIceComponent::setRemotePassword(const QString &password) +{ + m_remotePassword = password; +} + +/// Sets the list of sockets to use for this component. +/// +/// \param sockets + +void QXmppIceComponent::setSockets(QList<QUdpSocket*> sockets) +{ + // clear previous candidates and sockets + m_localCandidates.clear(); + foreach (QUdpSocket *socket, m_sockets) + delete socket; + m_sockets.clear(); + + // store candidates + int foundation = 0; + foreach (QUdpSocket *socket, sockets) + { + socket->setParent(this); + connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead())); + + QXmppJingleCandidate candidate; + candidate.setComponent(m_component); + candidate.setFoundation(foundation++); + // remove scope ID from IPv6 non-link local addresses + QHostAddress addr(socket->localAddress()); + if (addr.protocol() == QAbstractSocket::IPv6Protocol && + !isIPv6LinkLocalAddress(addr)) { + addr.setScopeId(QString()); + } + candidate.setHost(addr); + candidate.setId(generateStanzaHash(10)); + candidate.setPort(socket->localPort()); + candidate.setProtocol("udp"); + candidate.setType(QXmppJingleCandidate::HostType); + candidate.setPriority(candidatePriority(candidate)); + + m_sockets << socket; + m_localCandidates << candidate; + } + + // start STUN checks + if (!m_stunHost.isNull() && m_stunPort) { + m_stunTries = 0; + checkStun(); + m_stunTimer->start(); + } + + // connect to TURN server + if (m_turnConfigured) + m_turnAllocation->connectToHost(); +} + +/// Sets the STUN server to use to determine server-reflexive addresses +/// and ports. +/// +/// \param host The address of the STUN server. +/// \param port The port of the STUN server. + +void QXmppIceComponent::setStunServer(const QHostAddress &host, quint16 port) +{ + m_stunHost = host; + m_stunPort = port; + m_stunId = generateRandomBytes(ID_SIZE); +} + +/// Sets the TURN server to use to relay packets in double-NAT configurations. +/// +/// \param host The address of the TURN server. +/// \param port The port of the TURN server. + +void QXmppIceComponent::setTurnServer(const QHostAddress &host, quint16 port) +{ + m_turnAllocation->setServer(host, port); + m_turnConfigured = !host.isNull() && port; +} + +/// Sets the \a user used for authentication with the TURN server. +/// +/// \param user + +void QXmppIceComponent::setTurnUser(const QString &user) +{ + m_turnAllocation->setUser(user); +} + +/// Sets the \a password used for authentication with the TURN server. +/// +/// \param password + +void QXmppIceComponent::setTurnPassword(const QString &password) +{ + m_turnAllocation->setPassword(password); +} + +void QXmppIceComponent::readyRead() +{ + QUdpSocket *socket = qobject_cast<QUdpSocket*>(sender()); + if (!socket) + return; + + 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, socket); + } +} + +void QXmppIceComponent::handleDatagram(const QByteArray &buffer, const QHostAddress &remoteHost, quint16 remotePort, QUdpSocket *socket) +{ + // 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 + foreach (Pair *pair, m_pairs) { + if (pair->remote.host() == remoteHost && + pair->remote.port() == remotePort) { + m_fallbackPair = pair; + break; + } + } + emit datagramReceived(buffer); + return; + } + + // determine password to use + QString messagePassword; + if (messageId != m_stunId) + { + messagePassword = (messageType & 0xFF00) ? m_remotePassword : m_localPassword; + if (messagePassword.isEmpty()) + return; + } + + // parse STUN message + QXmppStunMessage message; + QStringList errors; + if (!message.decode(buffer, messagePassword.toUtf8(), &errors)) + { + foreach (const QString &error, errors) + warning(error); + return; + } +#ifdef QXMPP_DEBUG_STUN + logReceived(QString("STUN packet from %1 port %2\n%3").arg( + remoteHost.toString(), + QString::number(remotePort), + message.toString())); +#endif + + // check how to handle message + if (message.id() == m_stunId) + { + m_stunTimer->stop(); + + // determine server-reflexive address + QHostAddress reflexiveHost; + quint16 reflexivePort = 0; + if (!message.xorMappedHost.isNull() && message.xorMappedPort != 0) + { + reflexiveHost = message.xorMappedHost; + reflexivePort = message.xorMappedPort; + } + else if (!message.mappedHost.isNull() && message.mappedPort != 0) + { + reflexiveHost = message.mappedHost; + reflexivePort = message.mappedPort; + } else { + warning("STUN server did not provide a reflexive address"); + return; + } + + // check whether this candidates is already known + foreach (const QXmppJingleCandidate &candidate, m_localCandidates) + { + if (candidate.host() == reflexiveHost && + candidate.port() == reflexivePort && + candidate.type() == QXmppJingleCandidate::ServerReflexiveType) + return; + } + + // add the new local candidate + debug(QString("Adding server-reflexive candidate %1 port %2").arg(reflexiveHost.toString(), QString::number(reflexivePort))); + QXmppJingleCandidate candidate; + candidate.setComponent(m_component); + candidate.setHost(reflexiveHost); + candidate.setId(generateStanzaHash(10)); + candidate.setPort(reflexivePort); + candidate.setProtocol("udp"); + candidate.setType(QXmppJingleCandidate::ServerReflexiveType); + candidate.setPriority(candidatePriority(candidate)); + m_localCandidates << candidate; + + emit localCandidatesChanged(); + return; + } + + // process message from peer + Pair *pair = 0; + if (message.type() == (QXmppStunMessage::Binding | QXmppStunMessage::Request)) + { + // add remote candidate + pair = addRemoteCandidate(socket, remoteHost, remotePort, message.priority()); + + // send a binding response + QXmppStunMessage response; + response.setId(message.id()); + response.setType(QXmppStunMessage::Binding | QXmppStunMessage::Response); + response.setUsername(message.username()); + response.xorMappedHost = pair->remote.host(); + response.xorMappedPort = pair->remote.port(); + writeStun(response, pair); + + // update state + if (m_iceControlling || message.useCandidate) + { + debug(QString("ICE reverse check complete %1").arg(pair->toString())); + pair->checked |= QIODevice::ReadOnly; + } + + if (!m_iceControlling && !m_activePair && !m_remoteUser.isEmpty()) + { + // send a triggered connectivity test + QXmppStunMessage message; + message.setId(pair->transaction); + message.setType(QXmppStunMessage::Binding | QXmppStunMessage::Request); + message.setPriority(m_peerReflexivePriority); + message.setUsername(QString("%1:%2").arg(m_remoteUser, m_localUser)); + message.iceControlled = QByteArray(8, 0); + writeStun(message, pair); + } + + } else if (message.type() == (QXmppStunMessage::Binding | QXmppStunMessage::Response)) { + + // find the pair for this transaction + foreach (Pair *ptr, m_pairs) + { + if (ptr->transaction == message.id()) + { + pair = ptr; + break; + } + } + if (!pair) + { + debug(QString("Unknown transaction %1").arg(QString::fromAscii(message.id().toHex()))); + return; + } + // store peer-reflexive address + pair->reflexive.setHost(message.xorMappedHost); + pair->reflexive.setPort(message.xorMappedPort); + +#if 0 + // send a binding indication + QXmppStunMessage indication; + indication.setId(generateRandomBytes(ID_SIZE)); + indication.setType(BindingIndication); + m_socket->writeStun(indication, pair); +#endif + + // outgoing media can flow + debug(QString("ICE forward check complete %1").arg(pair->toString())); + pair->checked |= QIODevice::WriteOnly; + } + + // signal completion + if (pair && pair->checked == QIODevice::ReadWrite) + { + m_timer->stop(); + if (!m_activePair || pair->priority() > m_activePair->priority()) { + info(QString("ICE pair selected %1 (priority: %2)").arg( + pair->toString(), QString::number(pair->priority()))); + const bool wasConnected = (m_activePair != 0); + m_activePair = pair; + if (!wasConnected) + emit connected(); + } + } +} + +void QXmppIceComponent::turnConnected() +{ + // add the new local candidate + debug(QString("Adding relayed candidate %1 port %2").arg( + m_turnAllocation->relayedHost().toString(), + QString::number(m_turnAllocation->relayedPort()))); + QXmppJingleCandidate candidate; + candidate.setComponent(m_component); + candidate.setHost(m_turnAllocation->relayedHost()); + candidate.setId(generateStanzaHash(10)); + candidate.setPort(m_turnAllocation->relayedPort()); + candidate.setProtocol("udp"); + candidate.setType(QXmppJingleCandidate::RelayedType); + candidate.setPriority(candidatePriority(candidate)); + m_localCandidates << candidate; + + emit localCandidatesChanged(); +} + +static QList<QUdpSocket*> reservePort(const QList<QHostAddress> &addresses, quint16 port, QObject *parent) +{ + QList<QUdpSocket*> sockets; + foreach (const QHostAddress &address, addresses) { + QUdpSocket *socket = new QUdpSocket(parent); + sockets << socket; + if (!socket->bind(address, port)) { + for (int i = 0; i < sockets.size(); ++i) + delete sockets[i]; + sockets.clear(); + break; + } + } + return sockets; +} + +/// Returns the list of local network addresses. + +QList<QHostAddress> QXmppIceComponent::discoverAddresses() +{ + QList<QHostAddress> addresses; + foreach (const QNetworkInterface &interface, QNetworkInterface::allInterfaces()) + { + if (!(interface.flags() & QNetworkInterface::IsRunning) || + interface.flags() & QNetworkInterface::IsLoopBack) + continue; + + foreach (const QNetworkAddressEntry &entry, interface.addressEntries()) + { + QHostAddress ip = entry.ip(); + if ((ip.protocol() != QAbstractSocket::IPv4Protocol && + ip.protocol() != QAbstractSocket::IPv6Protocol) || + entry.netmask().isNull()) + continue; + +#ifdef Q_OS_MAC + // FIXME: on Mac OS X, sending IPv6 UDP packets fails + if (ip.protocol() == QAbstractSocket::IPv6Protocol) + continue; +#endif + + // 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. +/// +/// \param addresses The network address on which to bind the sockets. +/// \param count The number of ports to reserve. +/// \param parent 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) { + for (int i = 0; i < sockets.size(); ++i) + delete sockets[i]; + sockets.clear(); + } + } + return sockets; +} + +/// Sends a data packet to the remote party. +/// +/// \param datagram + +qint64 QXmppIceComponent::sendDatagram(const QByteArray &datagram) +{ + Pair *pair = m_activePair ? m_activePair : m_fallbackPair; + if (!pair) + return -1; + if (pair->socket) + return pair->socket->writeDatagram(datagram, pair->remote.host(), pair->remote.port()); + else if (m_turnAllocation->state() == QXmppTurnAllocation::ConnectedState) + return m_turnAllocation->writeDatagram(datagram, pair->remote.host(), pair->remote.port()); + else + return -1; +} + +/// Sends a STUN packet to the remote party. + +qint64 QXmppIceComponent::writeStun(const QXmppStunMessage &message, QXmppIceComponent::Pair *pair) +{ + qint64 ret; + const QString messagePassword = (message.type() & 0xFF00) ? m_localPassword : m_remotePassword; + if (pair->socket) + ret = pair->socket->writeDatagram( + message.encode(messagePassword.toUtf8()), + pair->remote.host(), + pair->remote.port()); + else if (m_turnAllocation->state() == QXmppTurnAllocation::ConnectedState) + ret = m_turnAllocation->writeDatagram( + message.encode(messagePassword.toUtf8()), + pair->remote.host(), + pair->remote.port()); + else + return -1; +#ifdef QXMPP_DEBUG_STUN + logSent(QString("Sent to %1\n%2").arg(pair->toString(), message.toString())); +#endif + return ret; +} + +/// Constructs a new ICE connection. +/// +/// \param controlling +/// \param parent + +QXmppIceConnection::QXmppIceConnection(QObject *parent) + : QXmppLoggable(parent), + m_iceControlling(false), + m_stunPort(0) +{ + bool check; + + m_localUser = generateStanzaHash(4); + m_localPassword = generateStanzaHash(22); + + // timer to limit connection time to 30 seconds + m_connectTimer = new QTimer(this); + m_connectTimer->setInterval(30000); + m_connectTimer->setSingleShot(true); + check = connect(m_connectTimer, SIGNAL(timeout()), + this, SLOT(slotTimeout())); + Q_ASSERT(check); + Q_UNUSED(check); +} + +/// Returns the given component of this ICE connection. +/// +/// \param component + +QXmppIceComponent *QXmppIceConnection::component(int component) +{ + return m_components.value(component); +} + +/// Adds a component to this ICE connection, for instance 1 for RTP +/// or 2 for RTCP. +/// +/// \param component + +void QXmppIceConnection::addComponent(int component) +{ + bool check; + Q_UNUSED(check); + + if (m_components.contains(component)) + { + warning(QString("Already have component %1").arg(QString::number(component))); + return; + } + + QXmppIceComponent *socket = new QXmppIceComponent(this); + socket->setComponent(component); + socket->setIceControlling(m_iceControlling); + socket->setLocalUser(m_localUser); + socket->setLocalPassword(m_localPassword); + socket->setStunServer(m_stunHost, m_stunPort); + socket->setTurnServer(m_turnHost, m_turnPort); + socket->setTurnUser(m_turnUser); + socket->setTurnPassword(m_turnPassword); + + check = connect(socket, SIGNAL(localCandidatesChanged()), + this, SIGNAL(localCandidatesChanged())); + Q_ASSERT(check); + + check = connect(socket, SIGNAL(connected()), + this, SLOT(slotConnected())); + Q_ASSERT(check); + + m_components[component] = socket; +} + +/// Adds a candidate for one of the remote components. +/// +/// \param candidate + +void QXmppIceConnection::addRemoteCandidate(const QXmppJingleCandidate &candidate) +{ + QXmppIceComponent *socket = m_components.value(candidate.component()); + if (!socket) + { + warning(QString("Not adding candidate for unknown component %1").arg( + QString::number(candidate.component()))); + return; + } + socket->addRemoteCandidate(candidate); +} + +/// Binds the local sockets to the specified addresses. +/// +/// \param addresses The addresses on which to listen. + +bool QXmppIceConnection::bind(const QList<QHostAddress> &addresses) +{ + // reserve ports + QList<QUdpSocket*> sockets = QXmppIceComponent::reservePorts(addresses, m_components.size()); + if (sockets.isEmpty() && !addresses.isEmpty()) + return false; + + // assign sockets + QList<int> keys = m_components.keys(); + qSort(keys); + int s = 0; + foreach (int k, keys) { + m_components[k]->setSockets(sockets.mid(s, addresses.size())); + s += addresses.size(); + } + + return true; +} + +/// Closes the ICE connection. + +void QXmppIceConnection::close() +{ + m_connectTimer->stop(); + foreach (QXmppIceComponent *socket, m_components.values()) + socket->close(); +} + +/// Starts ICE connectivity checks. + +void QXmppIceConnection::connectToHost() +{ + if (isConnected() || m_connectTimer->isActive()) + return; + + foreach (QXmppIceComponent *socket, m_components.values()) + socket->connectToHost(); + m_connectTimer->start(); +} + + +/// Returns true if ICE negotiation completed, false otherwise. + +bool QXmppIceConnection::isConnected() const +{ + foreach (QXmppIceComponent *socket, m_components.values()) + if (!socket->isConnected()) + return false; + return true; +} + +void QXmppIceConnection::setIceControlling(bool controlling) +{ + m_iceControlling = controlling; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setIceControlling(controlling); +} + +/// Returns the list of local HOST CANDIDATES candidates by iterating +/// over the available network interfaces. + +QList<QXmppJingleCandidate> QXmppIceConnection::localCandidates() const +{ + QList<QXmppJingleCandidate> candidates; + foreach (QXmppIceComponent *socket, m_components.values()) + candidates += socket->localCandidates(); + return candidates; +} + +/// Returns the local user fragment. + +QString QXmppIceConnection::localUser() const +{ + return m_localUser; +} + +/// Sets the local user fragment. +/// +/// You do not usually need to call this as one is automatically generated. +/// +/// \param user + +void QXmppIceConnection::setLocalUser(const QString &user) +{ + m_localUser = user; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setLocalUser(user); +} + +/// Returns the local password. + +QString QXmppIceConnection::localPassword() const +{ + return m_localPassword; +} + +/// Sets the local password. +/// +/// You do not usually need to call this as one is automatically generated. +/// +/// \param password + +void QXmppIceConnection::setLocalPassword(const QString &password) +{ + m_localPassword = password; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setLocalPassword(password); +} + +/// Sets the remote user fragment. +/// +/// \param user + +void QXmppIceConnection::setRemoteUser(const QString &user) +{ + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setRemoteUser(user); +} + +/// Sets the remote password. +/// +/// \param password + +void QXmppIceConnection::setRemotePassword(const QString &password) +{ + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setRemotePassword(password); +} + +/// Sets the STUN server to use to determine server-reflexive addresses +/// and ports. +/// +/// \param host The address of the STUN server. +/// \param port The port of the STUN server. + +void QXmppIceConnection::setStunServer(const QHostAddress &host, quint16 port) +{ + m_stunHost = host; + m_stunPort = port; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setStunServer(host, port); +} + +/// Sets the TURN server to use to relay packets in double-NAT configurations. +/// +/// \param host The address of the TURN server. +/// \param port The port of the TURN server. + +void QXmppIceConnection::setTurnServer(const QHostAddress &host, quint16 port) +{ + m_turnHost = host; + m_turnPort = port; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setTurnServer(host, port); +} + +/// Sets the \a user used for authentication with the TURN server. +/// +/// \param user + +void QXmppIceConnection::setTurnUser(const QString &user) +{ + m_turnUser = user; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setTurnUser(user); +} + +/// Sets the \a password used for authentication with the TURN server. +/// +/// \param password + +void QXmppIceConnection::setTurnPassword(const QString &password) +{ + m_turnPassword = password; + foreach (QXmppIceComponent *socket, m_components.values()) + socket->setTurnPassword(password); +} + +void QXmppIceConnection::slotConnected() +{ + foreach (QXmppIceComponent *socket, m_components.values()) + if (!socket->isConnected()) + return; + info(QString("ICE negotiation completed")); + m_connectTimer->stop(); + emit connected(); +} + +void QXmppIceConnection::slotTimeout() +{ + warning(QString("ICE negotiation timed out")); + foreach (QXmppIceComponent *socket, m_components.values()) + socket->close(); + emit disconnected(); +} + |
