aboutsummaryrefslogtreecommitdiff
path: root/source/QXmppStun.cpp
diff options
context:
space:
mode:
authorJeremy Lainé <jeremy.laine@m4x.org>2010-07-16 09:14:06 +0000
committerJeremy Lainé <jeremy.laine@m4x.org>2010-07-16 09:14:06 +0000
commitc30387412d11366587a6b46975169b2ddddb4375 (patch)
tree359f66390f416cab7fcf8d418188c7a0634d3fd4 /source/QXmppStun.cpp
parent82709f63191cf8da26dc0fd9be51b8fe68d6f6f0 (diff)
downloadqxmpp-c30387412d11366587a6b46975169b2ddddb4375.tar.gz
add support for STUN / ICE-UDP sockets
Diffstat (limited to 'source/QXmppStun.cpp')
-rw-r--r--source/QXmppStun.cpp682
1 files changed, 682 insertions, 0 deletions
diff --git a/source/QXmppStun.cpp b/source/QXmppStun.cpp
new file mode 100644
index 00000000..dc6603d1
--- /dev/null
+++ b/source/QXmppStun.cpp
@@ -0,0 +1,682 @@
+/*
+ * Copyright (C) 2010 Bolloré telecom
+ *
+ * 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.
+ *
+ */
+
+#include <QCryptographicHash>
+#include <QDebug>
+#include <QNetworkInterface>
+#include <QUdpSocket>
+#include <QTimer>
+
+#include "QXmppStun.h"
+#include "QXmppUtils.h"
+
+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 MessageType {
+ BindingRequest = 0x0001,
+ BindingIndication = 0x0011,
+ BindingResponse = 0x0101,
+ BindingError = 0x0111,
+ SharedSecretRequest = 0x0002,
+ SharedSecretResponse = 0x0102,
+ SharedSecretError = 0x0112,
+};
+
+enum AttributeType {
+ Username = 0x0006,
+ MessageIntegrity = 0x0008,
+ ErrorCode = 0x0009,
+ XorMappedAddress = 0x0020,
+ Priority = 0x0024,
+ UseCandidate = 0x0025,
+ Fingerprint = 0x8028,
+ IceControlled = 0x8029,
+ IceControlling = 0x802a,
+};
+
+static QByteArray randomByteArray(int length)
+{
+ QByteArray result(length, 0);
+ for (int i = 0; i < length; ++i)
+ result[i] = quint8(qrand() % 255);
+ return result;
+}
+
+class QXmppStunMessage
+{
+public:
+ QXmppStunMessage();
+
+ QByteArray encode(const QString &password = QString()) const;
+ bool decode(const QByteArray &buffer, const QString &password = QString());
+
+ static quint16 peekType(const QByteArray &buffer);
+
+ quint16 type;
+ QByteArray id;
+
+ // attributes
+ int errorCode;
+ QString errorPhrase;
+ quint32 priority;
+ QByteArray iceControlling;
+ QByteArray iceControlled;
+ QHostAddress mappedHost;
+ quint16 mappedPort;
+ QString username;
+ bool useCandidate;
+
+private:
+ void setBodyLength(QByteArray &buffer, qint16 length) const;
+};
+
+/// Constructs a new QXmppStunMessage.
+
+QXmppStunMessage::QXmppStunMessage()
+ : errorCode(0), priority(0), mappedPort(0), useCandidate(false)
+{
+}
+
+/// Decodes a QXmppStunMessage and checks its integrity using the given
+/// password.
+///
+/// \param buffer
+/// \param password
+
+bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password)
+{
+ if (buffer.size() < STUN_HEADER)
+ {
+ qWarning("QXmppStunMessage received a truncated STUN packet");
+ return false;
+ }
+
+ // parse STUN header
+ QDataStream stream(buffer);
+ quint16 length;
+ quint32 cookie;
+ stream >> type;
+ stream >> length;
+ stream >> cookie;
+ id.resize(12);
+ stream.readRawData(id.data(), id.size());
+
+ if (cookie != STUN_MAGIC || length != buffer.size() - STUN_HEADER)
+ {
+ qWarning("QXmppStunMessage received an invalid STUN packet");
+ return false;
+ }
+
+ // parse STUN attributes
+ int done = 0;
+ 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;
+ if (a_type == Priority)
+ {
+ if (a_length != sizeof(priority))
+ return false;
+ stream >> priority;
+ } else if (a_type == ErrorCode) {
+ 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) {
+ if (a_length != 0)
+ return false;
+ useCandidate = true;
+ } else if (a_type == XorMappedAddress) {
+ if (a_length < 4)
+ return false;
+ quint8 reserved, protocol;
+ quint16 xport;
+ stream >> reserved;
+ stream >> protocol;
+ stream >> xport;
+ mappedPort = xport ^ (STUN_MAGIC >> 16);
+ if (protocol == STUN_IPV4)
+ {
+ if (a_length != 8)
+ return false;
+ quint32 xaddr;
+ stream >> xaddr;
+ mappedHost = QHostAddress(xaddr ^ STUN_MAGIC);
+ } else if (protocol == STUN_IPV6) {
+ if (a_length != 20)
+ return false;
+ QByteArray xaddr(16, 0);
+ stream.readRawData(xaddr.data(), xaddr.size());
+ QByteArray xpad;
+ QDataStream(&xpad, QIODevice::WriteOnly) << STUN_MAGIC;
+ xpad += id;
+ Q_IPV6ADDR addr;
+ for (int i = 0; i < 16; i++)
+ addr[i] = xaddr[i] ^ xpad[i];
+ mappedHost = QHostAddress(addr);
+ } else {
+ qWarning("QXmppStunMessage bad protocol");
+ return false;
+ }
+ } else if (a_type == MessageIntegrity) {
+ if (a_length != 20)
+ return false;
+ QByteArray integrity(20, 0);
+ stream.readRawData(integrity.data(), integrity.size());
+ // check HMAC-SHA1
+ if (!password.isEmpty())
+ {
+ const QByteArray key = password.toUtf8();
+ QByteArray copy = buffer.left(STUN_HEADER + done);
+ setBodyLength(copy, done + 24);
+ if (integrity != generateHmacSha1(key, copy))
+ {
+ qWarning("QXmppStunMessage bad integrity");
+ return false;
+ }
+ }
+ } else if (a_type == 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)
+ {
+ qWarning("QXmppStunMessage bad fingerprint");
+ return false;
+ }
+ } else {
+ QByteArray a_value(a_length, 0);
+ stream.readRawData(a_value.data(), a_value.size());
+ if (a_type == Username)
+ {
+ username = QString::fromUtf8(a_value);
+ } else if (a_type == IceControlling) {
+ iceControlling = a_value;
+ } else if (a_type == IceControlled) {
+ iceControlled = a_value;
+ } else {
+ qWarning() << "QXmppStunMessage unknown attribute type" << a_type << "length" << a_length << "padding" << pad_length << "value" << a_value.toHex();
+ }
+ }
+ 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 password.
+///
+/// \param password
+
+QByteArray QXmppStunMessage::encode(const QString &password) const
+{
+ QByteArray buffer;
+ QDataStream stream(&buffer, QIODevice::WriteOnly);
+
+ // encode STUN header
+ quint16 length = 0;
+ stream << type;
+ stream << length;
+ stream << STUN_MAGIC;
+ stream.writeRawData(id.data(), id.size());
+
+ // XOR-MAPPED-ADDRESS
+ if (mappedPort && !mappedHost.isNull() &&
+ (mappedHost.protocol() == QAbstractSocket::IPv4Protocol ||
+ mappedHost.protocol() == QAbstractSocket::IPv6Protocol))
+ {
+ stream << quint16(XorMappedAddress);
+ stream << quint16(8);
+ stream << quint8(0);
+ if (mappedHost.protocol() == QAbstractSocket::IPv4Protocol)
+ {
+ stream << quint8(STUN_IPV4);
+ stream << quint16(mappedPort ^ (STUN_MAGIC >> 16));
+ stream << quint32(mappedHost.toIPv4Address() ^ STUN_MAGIC);
+ } else {
+ stream << quint8(STUN_IPV6);
+ stream << quint16(mappedPort ^ (STUN_MAGIC >> 16));
+ Q_IPV6ADDR addr = mappedHost.toIPv6Address();
+ QByteArray xaddr;
+ QDataStream(&xaddr, QIODevice::WriteOnly) << STUN_MAGIC;
+ xaddr += id;
+ for (int i = 0; i < 16; i++)
+ xaddr[i] = xaddr[i] ^ addr[i];
+ stream.writeRawData(xaddr.data(), xaddr.size());
+ }
+ }
+
+ // 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 (priority)
+ {
+ stream << quint16(Priority);
+ stream << quint16(sizeof(priority));
+ stream << priority;
+ }
+
+ // USE-CANDIDATE
+ if (useCandidate)
+ {
+ stream << quint16(UseCandidate);
+ stream << quint16(0);
+ }
+
+ // 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());
+ }
+
+ // USERNAME
+ if (!username.isEmpty())
+ {
+ const QByteArray user = username.toUtf8();
+ stream << quint16(Username);
+ stream << quint16(user.size());
+ stream.writeRawData(user.data(), user.size());
+ if (user.size() % 4)
+ {
+ const QByteArray padding(4 - (user.size() % 4), 0);
+ stream.writeRawData(padding.data(), padding.size());
+ }
+ }
+
+ // set body length
+ setBodyLength(buffer, buffer.size() - STUN_HEADER);
+
+ // integrity
+ if (!password.isEmpty())
+ {
+ const QByteArray key = password.toUtf8();
+ 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
+ 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
+
+quint16 QXmppStunMessage::peekType(const QByteArray &buffer)
+{
+ if (buffer.size() < STUN_HEADER)
+ return 0;
+
+ // parse STUN header
+ QDataStream stream(buffer);
+ quint16 type;
+ quint16 length;
+ quint32 cookie;
+ stream >> type;
+ stream >> length;
+ stream >> cookie;
+
+ if (cookie != STUN_MAGIC || length != buffer.size() - STUN_HEADER)
+ return 0;
+
+ return type;
+}
+
+void QXmppStunMessage::setBodyLength(QByteArray &buffer, qint16 length) const
+{
+ QDataStream stream(&buffer, QIODevice::WriteOnly);
+ stream.device()->seek(2);
+ stream << length;
+}
+
+/// Constructs a new QXmppStunSocket.
+///
+
+QXmppStunSocket::QXmppStunSocket(bool iceControlling, QObject *parent)
+ : QObject(parent),
+ m_openMode(QIODevice::NotOpen),
+ m_iceControlling(iceControlling),
+ m_remotePort(0)
+{
+ m_localUser = generateStanzaHash(4);
+ m_localPassword = generateStanzaHash(22);
+
+ m_socket = new QUdpSocket;
+ connect(m_socket, SIGNAL(readyRead()), this, SLOT(slotReadyRead()));
+ if (!m_socket->bind())
+ qWarning("QXmppStunSocket could not start listening");
+}
+
+/// Returns the component id for the current socket, e.g. 1 for RTP
+/// and 2 for RTCP.
+
+int QXmppStunSocket::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 QXmppStunSocket::setComponent(int component)
+{
+ m_component = component;
+}
+
+/// Closes the socket.
+
+void QXmppStunSocket::close()
+{
+ m_socket->close();
+}
+
+/// Start ICE connectivity checks.
+
+void QXmppStunSocket::connectToHost()
+{
+ if (!m_iceControlling)
+ return;
+
+ foreach (const QXmppJingleCandidate &candidate, m_remoteCandidates)
+ {
+ // send a binding request
+ QXmppStunMessage message;
+ message.id = randomByteArray(12);
+ message.type = BindingRequest;
+ // FIXME: calculate priority
+ message.priority = 1862270975;
+ message.username = QString("%1:%2").arg(m_remoteUser, m_localUser);
+ message.iceControlling = QByteArray(8, 0);
+ message.useCandidate = true;
+ dumpMessage(message, true, candidate.host(), candidate.port());
+ m_socket->writeDatagram(message.encode(m_remotePassword), candidate.host(), candidate.port());
+ }
+}
+
+/// Returns the QIODevice::OpenMode which represents the socket's ability
+/// to read and/or write data.
+
+QIODevice::OpenMode QXmppStunSocket::openMode() const
+{
+ return m_openMode;
+}
+
+void QXmppStunSocket::dumpMessage(const QXmppStunMessage &message, bool sent, const QHostAddress &host, quint16 port)
+{
+#ifdef QXMPP_DEBUG_STUN
+ qDebug() << "STUN(" << m_component << ")" << (sent ? "sent to" : "received from") << host.toString() << "port" << port;
+ QString typeName;
+ switch (message.type & 0x000f)
+ {
+ case 1: typeName = "Binding"; break;
+ case 2: typeName = "Shared Secret"; break;
+ default: typeName = "Unknown"; break;
+ }
+ switch (message.type & 0x0ff0)
+ {
+ case 0x000: typeName += " Request"; break;
+ case 0x010: typeName += " Indication"; break;
+ case 0x100: typeName += " Response"; break;
+ case 0x110: typeName += " Error"; break;
+ default: break;
+ }
+ qDebug() << " type" << typeName << " (" << message.type << ")";
+ qDebug() << " id " << message.id.toHex();
+
+ // attributes
+ if (!message.username.isEmpty())
+ qDebug() << " * username" << message.username;
+ if (message.errorCode)
+ qDebug() << " * error " << message.errorCode << message.errorPhrase;
+ if (message.mappedPort)
+ qDebug() << " * mapped " << message.mappedHost.toString() << message.mappedPort;
+#endif
+}
+
+/// Returns the list of local HOST CANDIDATES candidates by iterating
+/// over the available network interfaces.
+
+QList<QXmppJingleCandidate> QXmppStunSocket::localCandidates() const
+{
+ QList<QXmppJingleCandidate> candidates;
+ foreach (const QNetworkInterface &interface, QNetworkInterface::allInterfaces())
+ {
+ if (!(interface.flags() & QNetworkInterface::IsRunning) ||
+ interface.flags() & QNetworkInterface::IsLoopBack)
+ continue;
+
+ foreach (const QNetworkAddressEntry &entry, interface.addressEntries())
+ {
+ if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol ||
+ entry.netmask().isNull() ||
+ entry.netmask() == QHostAddress::Broadcast)
+ continue;
+
+ QXmppJingleCandidate candidate;
+ candidate.setComponent(m_component);
+ candidate.setHost(entry.ip());
+ candidate.setId(generateStanzaHash(10));
+ candidate.setNetwork(interface.index());
+ candidate.setPort(m_socket->localPort());
+ candidate.setPriority(2130706432 - m_component);
+ candidate.setProtocol("udp");
+ candidate.setType("host");
+ candidates << candidate;
+ }
+ }
+ return candidates;
+}
+
+QString QXmppStunSocket::localUser() const
+{
+ return m_localUser;
+}
+
+void QXmppStunSocket::setLocalUser(const QString &user)
+{
+ m_localUser = user;
+}
+
+QString QXmppStunSocket::localPassword() const
+{
+ return m_localPassword;
+}
+
+void QXmppStunSocket::setLocalPassword(const QString &password)
+{
+ m_localPassword = password;
+}
+
+/// Adds remote STUN candidates.
+
+void QXmppStunSocket::addRemoteCandidates(const QList<QXmppJingleCandidate> &candidates)
+{
+ foreach (const QXmppJingleCandidate &candidate, candidates)
+ {
+ if (candidate.component() == m_component &&
+ candidate.type() == "host" &&
+ candidate.protocol() == "udp")
+ m_remoteCandidates << candidate;
+ }
+}
+
+void QXmppStunSocket::setRemoteUser(const QString &user)
+{
+ m_remoteUser = user;
+}
+
+void QXmppStunSocket::setRemotePassword(const QString &password)
+{
+ m_remotePassword = password;
+}
+
+void QXmppStunSocket::slotReadyRead()
+{
+ const qint64 size = m_socket->pendingDatagramSize();
+ QHostAddress remoteHost;
+ quint16 remotePort;
+ QByteArray buffer(size, 0);
+ m_socket->readDatagram(buffer.data(), buffer.size(), &remoteHost, &remotePort);
+
+ // if this is not a STUN message, emit it
+ quint16 messageType = QXmppStunMessage::peekType(buffer);
+ if (!messageType)
+ {
+ emit datagramReceived(buffer, remoteHost, remotePort);
+ return;
+ }
+
+ const QString messagePassword = (messageType & 0xFF00) ? m_remotePassword : m_localPassword;
+ if (messagePassword.isEmpty())
+ return;
+ QXmppStunMessage message;
+ if (!message.decode(buffer, messagePassword))
+ return;
+ dumpMessage(message, false, remoteHost, remotePort);
+
+ if (m_openMode == QIODevice::ReadWrite)
+ return;
+
+ if (message.type == BindingRequest)
+ {
+ // send a binding response
+ QXmppStunMessage response;
+ response.id = message.id;
+ response.type = BindingResponse;
+ response.username = message.username;
+ response.mappedHost = remoteHost;
+ response.mappedPort = remotePort;
+ dumpMessage(response, true, remoteHost, remotePort);
+ m_socket->writeDatagram(response.encode(m_localPassword), remoteHost, remotePort);
+
+ if (m_iceControlling || message.useCandidate)
+ {
+ // outgoing media can flow
+ qDebug() << "STUN(" << m_component << ") OUTGOING MEDIA ENABLED";
+ m_openMode |= QIODevice::WriteOnly;
+ m_remoteHost = remoteHost;
+ m_remotePort = remotePort;
+ emit ready();
+ }
+
+ if (!m_iceControlling)
+ {
+ // send a triggered connectivity test
+ QXmppStunMessage message;
+ message.id = randomByteArray(12);
+ message.type = BindingRequest;
+ // FIXME : calculate priority
+ message.priority = 1862270975;
+ message.username = QString("%1:%2").arg(m_remoteUser, m_localUser);
+ message.iceControlled = QByteArray(8, 0);
+ dumpMessage(message, true, remoteHost, remotePort);
+ m_socket->writeDatagram(message.encode(m_remotePassword), remoteHost, remotePort);
+ }
+ } else if (message.type == BindingResponse) {
+ // send a binding indication
+ QXmppStunMessage indication;
+ indication.id = randomByteArray(12);
+ indication.type = BindingIndication;
+ dumpMessage(indication, true, remoteHost, remotePort);
+ m_socket->writeDatagram(indication.encode(), remoteHost, remotePort);
+
+ // incoming media can flow
+ qDebug() << "STUN(" << m_component << ") INCOMING MEDIA ENABLED";
+ m_openMode |= QIODevice::ReadOnly;
+ m_remoteHost = remoteHost;
+ m_remotePort = remotePort;
+
+ // ICE negotiation succeeded
+ if (m_iceControlling)
+ qDebug() << "STUN(" << m_component << ") ICE-CONTROLLING negotiation finished" << remoteHost << remotePort;
+ } else if (message.type == BindingIndication) {
+ // ICE negotiation succeded
+ if (!m_iceControlling)
+ qDebug() << "STUN(" << m_component << ") ICE-CONTROLLED negotiation finished" << remoteHost << remotePort;
+ }
+}
+
+/// Sends a data packet to the remote party.
+///
+/// \param datagram
+
+qint64 QXmppStunSocket::writeDatagram(const QByteArray &datagram)
+{
+ return m_socket->writeDatagram(datagram, m_remoteHost, m_remotePort);
+}
+