diff options
| author | Jeremy Lainé <jeremy.laine@m4x.org> | 2010-07-16 22:47:23 +0000 |
|---|---|---|
| committer | Jeremy Lainé <jeremy.laine@m4x.org> | 2010-07-16 22:47:23 +0000 |
| commit | 1f1d1e31597efd2acf6adbec2d5b33ecf78051a0 (patch) | |
| tree | 0001c783ced5a259b2dc62cc195fd8372e57a987 /source | |
| parent | 573491206b0bdb4ee2360d8c41b0da6a4bdb26b3 (diff) | |
| download | qxmpp-1f1d1e31597efd2acf6adbec2d5b33ecf78051a0.tar.gz | |
overhaul STUN / ICE
Diffstat (limited to 'source')
| -rw-r--r-- | source/QXmppStun.cpp | 428 | ||||
| -rw-r--r-- | source/QXmppStun.h | 36 |
2 files changed, 334 insertions, 130 deletions
diff --git a/source/QXmppStun.cpp b/source/QXmppStun.cpp index 1c69c30d..08a1c0a2 100644 --- a/source/QXmppStun.cpp +++ b/source/QXmppStun.cpp @@ -21,6 +21,8 @@ * */ +#define QXMPP_DEBUG_STUN + #include <QCryptographicHash> #include <QDebug> #include <QNetworkInterface> @@ -71,8 +73,8 @@ public: QXmppStunMessage(); QByteArray encode(const QString &password = QString()) const; - bool decode(const QByteArray &buffer, const QString &password = QString()); - + bool decode(const QByteArray &buffer, const QString &password = QString(), QStringList *errors = 0); + QString toString() const; static quint16 peekType(const QByteArray &buffer); quint16 type; @@ -106,11 +108,15 @@ QXmppStunMessage::QXmppStunMessage() /// \param buffer /// \param password -bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password) +bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password, QStringList *errors) { + QStringList silent; + if (!errors) + errors = &silent; + if (buffer.size() < STUN_HEADER) { - qWarning("QXmppStunMessage received a truncated STUN packet"); + *errors << QLatin1String("Received a truncated STUN packet"); return false; } @@ -126,24 +132,39 @@ bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password) if (cookie != STUN_MAGIC || length != buffer.size() - STUN_HEADER) { - qWarning("QXmppStunMessage received an invalid STUN packet"); + *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(priority)) return false; stream >> priority; + } else if (a_type == ErrorCode) { + + // ERROR-CODE if (a_length < 4) return false; quint16 reserved; @@ -155,11 +176,17 @@ bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password) 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 == XorMappedAddress) { + + // XOR-MAPPED-ADDRESS if (a_length < 4) return false; quint8 reserved, protocol; @@ -188,14 +215,18 @@ bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password) addr[i] = xaddr[i] ^ xpad[i]; mappedHost = QHostAddress(addr); } else { - qWarning("QXmppStunMessage bad protocol"); + *errors << QString("Bad protocol %1").arg(QString::number(protocol)); 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 (!password.isEmpty()) { @@ -204,37 +235,64 @@ bool QXmppStunMessage::decode(const QByteArray &buffer, const QString &password) setBodyLength(copy, done + 24); if (integrity != generateHmacSha1(key, copy)) { - qWarning("QXmppStunMessage bad integrity"); + *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) { - qWarning("QXmppStunMessage bad fingerprint"); + *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(8); + stream.readRawData(iceControlling.data(), iceControlling.size()); + + } else if (a_type == IceControlled) { + + /// ICE-CONTROLLED + if (a_length != 8) + return false; + iceControlled.resize(8); + stream.readRawData(iceControlled.data(), iceControlled.size()); + + } else if (a_type == Username) { + + // USERNAME + QByteArray utf8Username(a_length, 0); + stream.readRawData(utf8Username.data(), utf8Username.size()); + username = QString::fromUtf8(utf8Username); + } 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(); - } + + // 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; @@ -349,7 +407,7 @@ QByteArray QXmppStunMessage::encode(const QString &password) const // set body length setBodyLength(buffer, buffer.size() - STUN_HEADER); - // integrity + // MESSAGE-INTEGRITY if (!password.isEmpty()) { const QByteArray key = password.toUtf8(); @@ -360,7 +418,7 @@ QByteArray QXmppStunMessage::encode(const QString &password) const stream.writeRawData(integrity.data(), integrity.size()); } - // fingerprint + // FINGERPRINT setBodyLength(buffer, buffer.size() - STUN_HEADER + 8); quint32 fingerprint = generateCrc32(buffer) ^ 0x5354554eL; stream << quint16(Fingerprint); @@ -402,22 +460,91 @@ void QXmppStunMessage::setBodyLength(QByteArray &buffer, qint16 length) const stream << length; } +QString QXmppStunMessage::toString() const +{ + QStringList dumpLines; + QString typeName; + switch (type & 0x000f) + { + case 1: typeName = "Binding"; break; + case 2: typeName = "Shared Secret"; break; + default: typeName = "Unknown"; break; + } + switch (type & 0x0ff0) + { + case 0x000: typeName += " Request"; break; + case 0x010: typeName += " Indication"; break; + case 0x100: typeName += " Response"; break; + case 0x110: typeName += " Error"; break; + default: break; + } + dumpLines << QString(" type %1 (%2)") + .arg(typeName) + .arg(QString::number(type)); + dumpLines << QString(" id %1").arg(QString::fromAscii(id.toHex())); + + // attributes + if (!username.isEmpty()) + dumpLines << QString(" * USERNAME %1").arg(username); + if (errorCode) + dumpLines << QString(" * ERROR-CODE %1 %2") + .arg(QString::number(errorCode)) + .arg(errorPhrase); + if (mappedPort) + dumpLines << QString(" * MAPPED %1 %2") + .arg(mappedHost.toString()) + .arg(QString::number(mappedPort)); + 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"); +} + +QXmppStunSocket::Pair::Pair() + : checked(QIODevice::NotOpen) +{ + // FIXME : calculate priority + priority = 1862270975; + transaction = randomByteArray(12); +} + +QString QXmppStunSocket::Pair::toString() const +{ + QString str = QString("%1 %2").arg(remote.host().toString(), QString::number(remote.port())); + if (!reflexive.host().isNull() && reflexive.port()) + str += QString(" (reflexive %1 %2)").arg(reflexive.host().toString(), QString::number(reflexive.port())); + return str; +} + /// Constructs a new QXmppStunSocket. /// QXmppStunSocket::QXmppStunSocket(bool iceControlling, QObject *parent) : QObject(parent), - m_openMode(QIODevice::NotOpen), - m_iceControlling(iceControlling), - m_remotePort(0) + m_activePair(0), + m_iceControlling(iceControlling) { m_localUser = generateStanzaHash(4); m_localPassword = generateStanzaHash(22); - m_socket = new QUdpSocket; - connect(m_socket, SIGNAL(readyRead()), this, SLOT(slotReadyRead())); + m_socket = new QUdpSocket(this); + connect(m_socket, SIGNAL(readyRead()), this, SLOT(readyRead())); if (!m_socket->bind()) qWarning("QXmppStunSocket could not start listening"); + + m_timer = new QTimer(this); + m_timer->setInterval(500); + connect(m_timer, SIGNAL(timeout()), this, SLOT(checkCandidates())); +} + +QXmppStunSocket::~QXmppStunSocket() +{ + foreach (Pair *pair, m_pairs) + delete pair; } /// Returns the component id for the current socket, e.g. 1 for RTP @@ -438,34 +565,43 @@ void QXmppStunSocket::setComponent(int component) m_component = component; } +void QXmppStunSocket::checkCandidates() +{ + debug("Checking remote candidates"); + foreach (Pair *pair, m_pairs) + { + // send a binding request + QXmppStunMessage message; + message.id = pair->transaction; + message.type = BindingRequest; + message.priority = pair->priority; + message.username = 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); + } + // REMOTE PWD + writeStun(message, pair); + } +} + /// Closes the socket. void QXmppStunSocket::close() { m_socket->close(); + m_timer->stop(); } /// 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()); - } + checkCandidates(); + m_timer->start(); } /// Returns the QIODevice::OpenMode which represents the socket's ability @@ -473,44 +609,12 @@ void QXmppStunSocket::connectToHost() QIODevice::OpenMode QXmppStunSocket::openMode() const { - return m_openMode; + return m_activePair ? QIODevice::ReadWrite : QIODevice::NotOpen; } -void QXmppStunSocket::dumpMessage(const QXmppStunMessage &message, bool sent, const QHostAddress &host, quint16 port) +void QXmppStunSocket::debug(const QString &message, QXmppLogger::MessageType type) { -#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; -#else - Q_UNUSED(message); - Q_UNUSED(sent); - Q_UNUSED(host); - Q_UNUSED(port); -#endif + emit logMessage(type, QString("STUN(%1) %2").arg(QString::number(m_component)).arg(message)); } /// Returns the list of local HOST CANDIDATES candidates by iterating @@ -567,17 +671,47 @@ void QXmppStunSocket::setLocalPassword(const QString &password) m_localPassword = password; } -/// Adds remote STUN candidates. +/// Adds a remote STUN candidate. -void QXmppStunSocket::addRemoteCandidates(const QList<QXmppJingleCandidate> &candidates) +bool QXmppStunSocket::addRemoteCandidate(const QXmppJingleCandidate &candidate) { - foreach (const QXmppJingleCandidate &candidate, candidates) - { - if (candidate.component() == m_component && - candidate.type() == "host" && - candidate.protocol() == "udp") - m_remoteCandidates << candidate; - } + if (candidate.component() != m_component || + candidate.type() != "host" || + candidate.protocol() != "udp") + return false; + + foreach (Pair *pair, m_pairs) + if (pair->remote.host() == candidate.host() && + pair->remote.port() == candidate.port()) + return false; + + Pair *pair = new Pair; + pair->remote = candidate; + m_pairs << pair; + return true; +} + +/// Adds a discovered STUN candidate. + +QXmppStunSocket::Pair *QXmppStunSocket::addRemoteCandidate(const QHostAddress &host, quint16 port) +{ + foreach (Pair *pair, m_pairs) + if (pair->remote.host() == host && + pair->remote.port() == port) + return pair; + + QXmppJingleCandidate candidate; + candidate.setHost(host); + candidate.setPort(port); + candidate.setProtocol("udp"); + candidate.setComponent(m_component); + + Pair *pair = new Pair; + pair->remote = candidate; + m_pairs << pair; + + debug(QString("Added candidate %1").arg(pair->toString())); + return pair; } void QXmppStunSocket::setRemoteUser(const QString &user) @@ -590,7 +724,7 @@ void QXmppStunSocket::setRemotePassword(const QString &password) m_remotePassword = password; } -void QXmppStunSocket::slotReadyRead() +void QXmppStunSocket::readyRead() { const qint64 size = m_socket->pendingDatagramSize(); QHostAddress remoteHost; @@ -606,73 +740,106 @@ void QXmppStunSocket::slotReadyRead() return; } + // parse STUN message const QString messagePassword = (messageType & 0xFF00) ? m_remotePassword : m_localPassword; if (messagePassword.isEmpty()) return; QXmppStunMessage message; - if (!message.decode(buffer, messagePassword)) + QStringList errors; + if (!message.decode(buffer, messagePassword, &errors)) + { + foreach (const QString &error, errors) + debug(error, QXmppLogger::WarningMessage); return; - dumpMessage(message, false, remoteHost, remotePort); + } +#ifdef QXMPP_DEBUG_STUN + debug(QString("Received from %1 port %2\n%3") + .arg(remoteHost.toString()) + .arg(QString::number(remotePort)) + .arg(message.toString()), + QXmppLogger::ReceivedMessage); +#endif - if (m_openMode == QIODevice::ReadWrite) + if (m_activePair) return; + // process message + Pair *pair = 0; if (message.type == BindingRequest) { + // add remote candidate + pair = addRemoteCandidate(remoteHost, remotePort); + // 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); + response.mappedHost = pair->remote.host(); + response.mappedPort = pair->remote.port(); + writeStun(response, pair); + // update state 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(); + debug(QString("ICE reverse check %1").arg(pair->toString())); + pair->checked |= QIODevice::ReadOnly; } if (!m_iceControlling) { // send a triggered connectivity test QXmppStunMessage message; - message.id = randomByteArray(12); + message.id = pair->transaction; message.type = BindingRequest; - // FIXME : calculate priority - message.priority = 1862270975; + message.priority = pair->priority; 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); + writeStun(message, pair); } + } else if (message.type == BindingResponse) { + + // 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 reflexive address + pair->reflexive.setHost(message.mappedHost); + pair->reflexive.setPort(message.mappedPort); + + // add the new remote candidate + addRemoteCandidate(remoteHost, remotePort); + +#if 0 // 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; + m_socket->writeStun(indication, pair); +#endif + // outgoing media can flow + debug(QString("ICE forward check %1").arg(pair->toString())); + pair->checked |= QIODevice::WriteOnly; + } - // 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; + // signal completion + if (pair && pair->checked == QIODevice::ReadWrite) + { + debug(QString("ICE completed %1").arg(pair->toString())); + m_activePair = pair; + m_timer->stop(); + emit ready(); } } @@ -682,6 +849,21 @@ void QXmppStunSocket::slotReadyRead() qint64 QXmppStunSocket::writeDatagram(const QByteArray &datagram) { - return m_socket->writeDatagram(datagram, m_remoteHost, m_remotePort); + if (!m_activePair) + return -1; + return m_socket->writeDatagram(datagram, m_activePair->remote.host(), m_activePair->remote.port()); +} + +/// Sends a STUN packet to the remote party. + +qint64 QXmppStunSocket::writeStun(const QXmppStunMessage &message, QXmppStunSocket::Pair *pair) +{ + const QString messagePassword = (message.type & 0xFF00) ? m_localPassword : m_remotePassword; +#ifdef QXMPP_DEBUG_STUN + debug( + QString("Sent to %1\n%2").arg(pair->toString(), message.toString()), + QXmppLogger::SentMessage); +#endif + m_socket->writeDatagram(message.encode(messagePassword), pair->remote.host(), pair->remote.port()); } diff --git a/source/QXmppStun.h b/source/QXmppStun.h index d383fdd9..507723f7 100644 --- a/source/QXmppStun.h +++ b/source/QXmppStun.h @@ -26,9 +26,11 @@ #include <QObject> +#include "QXmppLogger.h" #include "QXmppJingleIq.h" class QUdpSocket; +class QTimer; class QXmppStunMessage; /// \brief The QXmppStunSocket class represents an UDP socket capable @@ -41,6 +43,7 @@ class QXmppStunSocket : public QObject public: QXmppStunSocket(bool iceControlling, QObject *parent=0); + ~QXmppStunSocket(); QList<QXmppJingleCandidate> localCandidates() const; QString localUser() const; @@ -51,7 +54,7 @@ public: int component() const; void setComponent(int component); - void addRemoteCandidates(const QList<QXmppJingleCandidate> &candidates); + bool addRemoteCandidate(const QXmppJingleCandidate &candidate); void setRemoteUser(const QString &user); void setRemotePassword(const QString &password); @@ -61,29 +64,48 @@ public: qint64 writeDatagram(const QByteArray &datagram); private slots: - void slotReadyRead(); + void checkCandidates(); + void readyRead(); signals: + // This signal is emitted when a data packet is received. void datagramReceived(const QByteArray &datagram, const QHostAddress &host, quint16 port); + + /// This signal is emitted to send logging messages. + void logMessage(QXmppLogger::MessageType type, const QString &msg); + void ready(); private: - void dumpMessage(const QXmppStunMessage &message, bool sent, const QHostAddress &host, quint16 port); + class Pair { + public: + Pair(); + QString toString() const; + + QIODevice::OpenMode checked; + quint32 priority; + QXmppJingleCandidate remote; + QXmppJingleCandidate reflexive; + QByteArray transaction; + }; + + Pair *addRemoteCandidate(const QHostAddress &host, quint16 port); + void debug(const QString &message, QXmppLogger::MessageType = QXmppLogger::DebugMessage); + qint64 writeStun(const QXmppStunMessage &message, QXmppStunSocket::Pair *pair); int m_component; - QIODevice::OpenMode m_openMode; QString m_localUser; QString m_localPassword; + Pair *m_activePair; bool m_iceControlling; - QList<QXmppJingleCandidate> m_remoteCandidates; - QHostAddress m_remoteHost; - quint16 m_remotePort; + QList<Pair*> m_pairs; QString m_remoteUser; QString m_remotePassword; QUdpSocket *m_socket; + QTimer *m_timer; }; #endif |
