aboutsummaryrefslogtreecommitdiff
path: root/src/QXmppStun.cpp
diff options
context:
space:
mode:
authorJeremy Lainé <jeremy.laine@m4x.org>2011-03-23 17:44:13 +0000
committerJeremy Lainé <jeremy.laine@m4x.org>2011-03-23 17:44:13 +0000
commitc604d85c48b0364517111ab09b4ffd412f84ec04 (patch)
tree5fa24adb7bd3ae2cedb481a40b19bf2e600d566e /src/QXmppStun.cpp
parentc5096e679d241b09c2b5096813ea7d6271ef84df (diff)
downloadqxmpp-c604d85c48b0364517111ab09b4ffd412f84ec04.tar.gz
use actual STUN transactions for TURN operations (handles retries on lost packets)
Diffstat (limited to 'src/QXmppStun.cpp')
-rw-r--r--src/QXmppStun.cpp320
1 files changed, 226 insertions, 94 deletions
diff --git a/src/QXmppStun.cpp b/src/QXmppStun.cpp
index be5e363c..6353b742 100644
--- a/src/QXmppStun.cpp
+++ b/src/QXmppStun.cpp
@@ -33,6 +33,8 @@
#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;
@@ -1062,6 +1064,76 @@ QString QXmppStunMessage::toString() const
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;
+
+ 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
@@ -1074,12 +1146,25 @@ QXmppTurnAllocation::QXmppTurnAllocation(QObject *parent)
m_lifetime(600),
m_state(UnconnectedState)
{
+ bool check;
+
socket = new QUdpSocket(this);
- connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead()));
+ 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);
- timer = new QTimer(this);
- timer->setSingleShot(true);
- connect(timer, SIGNAL(timeout()), this, SLOT(refresh()));
+ // 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);
}
/// Binds the local socket.
@@ -1095,6 +1180,9 @@ void QXmppTurnAllocation::close()
{
disconnectFromHost();
socket->close();
+ foreach (QXmppStunTransaction *transaction, m_transactions)
+ delete transaction;
+ m_transactions.clear();
setState(UnconnectedState);
}
@@ -1107,11 +1195,11 @@ void QXmppTurnAllocation::connectToHost()
// send allocate request
QXmppStunMessage request;
- request.setType(QXmppStunMessage::Allocate);
+ request.setType(QXmppStunMessage::Allocate | QXmppStunMessage::Request);
request.setId(generateRandomBytes(12));
request.setLifetime(m_lifetime);
request.setRequestedTransport(0x11);
- writeStun(request);
+ m_transactions << new QXmppStunTransaction(request, this);
// update state
setState(ConnectingState);
@@ -1121,19 +1209,20 @@ void QXmppTurnAllocation::connectToHost()
void QXmppTurnAllocation::disconnectFromHost()
{
- timer->stop();
+ m_channelTimer->stop();
+ m_timer->stop();
if (m_state != ConnectedState)
return;
// send refresh request with zero lifetime
QXmppStunMessage request;
- request.setType(QXmppStunMessage::Refresh);
+ 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);
- writeStun(request);
+ m_transactions << new QXmppStunTransaction(request, this);
// update state
setState(ClosingState);
@@ -1177,91 +1266,45 @@ void QXmppTurnAllocation::readyRead()
message.toString()));
#endif
- // handle authentication
- if (message.messageClass() == QXmppStunMessage::Error &&
- message.errorCode == 401 &&
- message.id() == m_request.id())
- {
- if (m_nonce != message.nonce() ||
- m_realm != message.realm()) {
- // update long-term credentials
- m_nonce = message.nonce();
- m_realm = message.realm();
- QCryptographicHash hash(QCryptographicHash::Md5);
- hash.addData((m_username + ":" + m_realm + ":" + m_password).toUtf8());
- m_key = hash.result();
-
- // retry request
- QXmppStunMessage request(m_request);
- request.setId(generateRandomBytes(12));
- request.setNonce(m_nonce);
- request.setRealm(m_realm);
- request.setUsername(m_username);
- writeStun(request);
- return;
- }
- }
-
- if (message.messageMethod() == QXmppStunMessage::Allocate) {
- if (message.messageClass() == QXmppStunMessage::Error) {
- warning(QString("Allocation failed: %1 %2").arg(
- QString::number(message.errorCode), message.errorPhrase));
- setState(UnconnectedState);
- return;
- }
- if (message.xorRelayedHost.isNull() ||
- message.xorRelayedHost.protocol() != QAbstractSocket::IPv4Protocol ||
- !message.xorRelayedPort) {
- warning("Allocation did not yield a valid relayed address");
- setState(UnconnectedState);
- return;
- }
-
- // store relayed address
- m_relayedHost = message.xorRelayedHost;
- m_relayedPort = message.xorRelayedPort;
-
- // schedule refresh
- m_lifetime = message.lifetime();
- timer->start((m_lifetime - 60) * 1000);
-
- setState(ConnectedState);
-
- } else if (message.messageMethod() == QXmppStunMessage::ChannelBind) {
- if (message.messageClass() == QXmppStunMessage::Error) {
- warning(QString("ChannelBind failed: %1 %2").arg(
- QString::number(message.errorCode), message.errorPhrase));
- return;
- }
-
- } else if (message.messageMethod() == QXmppStunMessage::Refresh) {
- if (message.messageClass() == QXmppStunMessage::Error) {
- warning(QString("Refresh failed: %1 %2").arg(
- QString::number(message.errorCode), message.errorPhrase));
- setState(UnconnectedState);
- return;
- }
-
- if (m_state == ClosingState) {
- setState(UnconnectedState);
+ // find transaction
+ foreach (QXmppStunTransaction *transaction, m_transactions) {
+ if (transaction->request().id() == message.id() &&
+ transaction->request().messageMethod() == message.messageMethod()) {
+ transaction->readStun(message);
return;
}
-
- // schedule refresh
- m_lifetime = message.lifetime();
- timer->start((m_lifetime - 60) * 1000);
}
}
+/// Refresh allocation.
+
void QXmppTurnAllocation::refresh()
{
QXmppStunMessage request;
- request.setType(QXmppStunMessage::Refresh);
+ request.setType(QXmppStunMessage::Refresh | QXmppStunMessage::Request);
request.setId(generateRandomBytes(12));
request.setNonce(m_nonce);
request.setRealm(m_realm);
request.setUsername(m_username);
- writeStun(request);
+ 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
@@ -1325,11 +1368,102 @@ void QXmppTurnAllocation::setState(AllocationState state)
if (m_state == ConnectedState) {
emit connected();
} else if (m_state == UnconnectedState) {
- timer->stop();
+ 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)
@@ -1340,10 +1474,11 @@ qint64 QXmppTurnAllocation::writeDatagram(const QByteArray &data, const QHostAdd
if (!channel) {
channel = m_channelNumber++;
+ m_channels.insert(channel, addr);
- // create channel
+ // bind channel
QXmppStunMessage request;
- request.setType(QXmppStunMessage::ChannelBind);
+ request.setType(QXmppStunMessage::ChannelBind | QXmppStunMessage::Request);
request.setId(generateRandomBytes(12));
request.setNonce(m_nonce);
request.setRealm(m_realm);
@@ -1351,10 +1486,11 @@ qint64 QXmppTurnAllocation::writeDatagram(const QByteArray &data, const QHostAdd
request.setChannelNumber(channel);
request.xorPeerHost = host;
request.xorPeerPort = port;
- if (writeStun(request) < 0)
- return -1;
+ m_transactions << new QXmppStunTransaction(request, this);
- m_channels.insert(channel, addr);
+ // schedule refresh
+ if (!m_channelTimer->isActive())
+ m_channelTimer->start();
}
// send data
@@ -1370,19 +1506,15 @@ qint64 QXmppTurnAllocation::writeDatagram(const QByteArray &data, const QHostAdd
return -1;
}
-qint64 QXmppTurnAllocation::writeStun(const QXmppStunMessage &message)
+void QXmppTurnAllocation::writeStun(const QXmppStunMessage &message)
{
- qint64 ret = socket->writeDatagram(message.encode(m_key),
- m_turnHost, m_turnPort);
- if (message.messageClass() == QXmppStunMessage::Request)
- m_request = 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
- return ret;
}
QXmppIceComponent::Pair::Pair(int component, bool controlling)