aboutsummaryrefslogtreecommitdiff
path: root/src/base/QXmppStun.cpp
diff options
context:
space:
mode:
authorJeremy Lainé <jeremy.laine@m4x.org>2012-02-08 12:51:15 +0000
committerJeremy Lainé <jeremy.laine@m4x.org>2012-02-08 12:51:15 +0000
commitdeb9d6cb53057ca8b90d10d6a3bdc5dcfd1b3ee4 (patch)
treed956bad28e28aadc3c83dbf88b3eddb5e1d9a9f4 /src/base/QXmppStun.cpp
parente8a1ad0cc608f12874ba4bafbd8282fa537ec9fb (diff)
downloadqxmpp-deb9d6cb53057ca8b90d10d6a3bdc5dcfd1b3ee4.tar.gz
move files common to client/server into "base"
Diffstat (limited to 'src/base/QXmppStun.cpp')
-rw-r--r--src/base/QXmppStun.cpp2581
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();
+}
+