226d06402b
The system was just treating IP (and optionally port) as a unique identifier, so if a peer had multiple possible paths to a client they would connect multiple times. This fixes that by generating using QUuid in each client. We then use this during broadcast, replacing the username we sent before (which was not used), and as part of the greeting. The greeting now is more complex, since we need to send both username and the ID. Change-Id: I6c6c2ffd5198406aad48445a68dd6aab36de69c0 Reviewed-by: Konrad Kujawa <konrad.kujawa@qt.io> Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
251 lines
6.8 KiB
C++
251 lines
6.8 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// Copyright (C) 2018 Intel Corporation.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
|
|
|
#include "connection.h"
|
|
|
|
#include <QTimerEvent>
|
|
|
|
static const int TransferTimeout = 30 * 1000;
|
|
static const int PongTimeout = 60 * 1000;
|
|
static const int PingInterval = 5 * 1000;
|
|
|
|
/*
|
|
* Protocol is defined as follows, using the CBOR Data Definition Language:
|
|
*
|
|
* protocol = [
|
|
* greeting, ; must start with a greeting command
|
|
* * command ; zero or more regular commands after
|
|
* ]
|
|
* command = plaintext / ping / pong / greeting
|
|
* plaintext = { 0 => text }
|
|
* ping = { 1 => null }
|
|
* pong = { 2 => null }
|
|
* greeting = { 3 => { text, bytes } }
|
|
*/
|
|
|
|
Connection::Connection(QObject *parent)
|
|
: QTcpSocket(parent), writer(this)
|
|
{
|
|
pingTimer.setInterval(PingInterval);
|
|
|
|
connect(this, &QTcpSocket::readyRead, this,
|
|
&Connection::processReadyRead);
|
|
connect(this, &QTcpSocket::disconnected,
|
|
&pingTimer, &QTimer::stop);
|
|
connect(&pingTimer, &QTimer::timeout,
|
|
this, &Connection::sendPing);
|
|
connect(this, &QTcpSocket::connected,
|
|
this, &Connection::sendGreetingMessage);
|
|
}
|
|
|
|
Connection::Connection(qintptr socketDescriptor, QObject *parent)
|
|
: Connection(parent)
|
|
{
|
|
setSocketDescriptor(socketDescriptor);
|
|
reader.setDevice(this);
|
|
}
|
|
|
|
Connection::~Connection()
|
|
{
|
|
if (isGreetingMessageSent && QAbstractSocket::state() != QAbstractSocket::UnconnectedState) {
|
|
// Indicate clean shutdown.
|
|
writer.endArray();
|
|
waitForBytesWritten(2000);
|
|
}
|
|
}
|
|
|
|
QString Connection::name() const
|
|
{
|
|
return username;
|
|
}
|
|
|
|
void Connection::setGreetingMessage(const QString &message, const QByteArray &uniqueId)
|
|
{
|
|
greetingMessage = message;
|
|
localUniqueId = uniqueId;
|
|
}
|
|
|
|
QByteArray Connection::uniqueId() const
|
|
{
|
|
return peerUniqueId;
|
|
}
|
|
|
|
bool Connection::sendMessage(const QString &message)
|
|
{
|
|
if (message.isEmpty())
|
|
return false;
|
|
|
|
writer.startMap(1);
|
|
writer.append(PlainText);
|
|
writer.append(message);
|
|
writer.endMap();
|
|
return true;
|
|
}
|
|
|
|
void Connection::timerEvent(QTimerEvent *timerEvent)
|
|
{
|
|
if (timerEvent->timerId() == transferTimerId) {
|
|
abort();
|
|
killTimer(transferTimerId);
|
|
transferTimerId = -1;
|
|
}
|
|
}
|
|
|
|
void Connection::processReadyRead()
|
|
{
|
|
// we've got more data, let's parse
|
|
reader.reparse();
|
|
while (reader.lastError() == QCborError::NoError) {
|
|
if (state == WaitingForGreeting) {
|
|
if (!reader.isArray())
|
|
break; // protocol error
|
|
|
|
reader.enterContainer(); // we'll be in this array forever
|
|
state = ReadingGreeting;
|
|
} else if (reader.containerDepth() == 1) {
|
|
// Current state: no command read
|
|
// Next state: read command ID
|
|
if (!reader.hasNext()) {
|
|
reader.leaveContainer();
|
|
disconnectFromHost();
|
|
return;
|
|
}
|
|
|
|
if (!reader.isMap() || !reader.isLengthKnown() || reader.length() != 1)
|
|
break; // protocol error
|
|
reader.enterContainer();
|
|
} else if (currentDataType == Undefined) {
|
|
// Current state: read command ID
|
|
// Next state: read command payload
|
|
if (!reader.isInteger())
|
|
break; // protocol error
|
|
currentDataType = DataType(reader.toInteger());
|
|
reader.next();
|
|
} else {
|
|
// Current state: read command payload
|
|
if (currentDataType == Greeting) {
|
|
if (state == ReadingGreeting) {
|
|
if (!reader.isContainer() || !reader.isLengthKnown() || reader.length() != 2)
|
|
break; // protocol error
|
|
state = ProcessingGreeting;
|
|
reader.enterContainer();
|
|
}
|
|
if (state != ProcessingGreeting)
|
|
break; // protocol error
|
|
if (reader.isString()) {
|
|
auto r = reader.readString();
|
|
buffer += r.data;
|
|
} else if (reader.isByteArray()) {
|
|
auto r = reader.readByteArray();
|
|
peerUniqueId += r.data;
|
|
if (r.status == QCborStreamReader::EndOfString) {
|
|
reader.leaveContainer();
|
|
processGreeting();
|
|
}
|
|
}
|
|
if (state == ProcessingGreeting)
|
|
continue;
|
|
} else if (reader.isString()) {
|
|
auto r = reader.readString();
|
|
buffer += r.data;
|
|
if (r.status != QCborStreamReader::EndOfString)
|
|
continue;
|
|
} else if (reader.isNull()) {
|
|
reader.next();
|
|
} else {
|
|
break; // protocol error
|
|
}
|
|
|
|
// Next state: no command read
|
|
reader.leaveContainer();
|
|
if (transferTimerId != -1) {
|
|
killTimer(transferTimerId);
|
|
transferTimerId = -1;
|
|
}
|
|
|
|
processData();
|
|
}
|
|
}
|
|
|
|
if (reader.lastError() != QCborError::EndOfFile)
|
|
abort(); // parse error
|
|
|
|
if (transferTimerId != -1 && reader.containerDepth() > 1)
|
|
transferTimerId = startTimer(TransferTimeout);
|
|
}
|
|
|
|
void Connection::sendPing()
|
|
{
|
|
if (pongTime.elapsed() > PongTimeout) {
|
|
abort();
|
|
return;
|
|
}
|
|
|
|
writer.startMap(1);
|
|
writer.append(Ping);
|
|
writer.append(nullptr); // no payload
|
|
writer.endMap();
|
|
}
|
|
|
|
void Connection::sendGreetingMessage()
|
|
{
|
|
writer.startArray(); // this array never ends
|
|
|
|
writer.startMap(1);
|
|
writer.append(Greeting);
|
|
writer.startArray(2);
|
|
writer.append(greetingMessage);
|
|
writer.append(localUniqueId);
|
|
writer.endArray();
|
|
writer.endMap();
|
|
isGreetingMessageSent = true;
|
|
|
|
if (!reader.device())
|
|
reader.setDevice(this);
|
|
}
|
|
|
|
void Connection::processGreeting()
|
|
{
|
|
username = buffer + '@' + peerAddress().toString() + ':'
|
|
+ QString::number(peerPort());
|
|
currentDataType = Undefined;
|
|
buffer.clear();
|
|
|
|
if (!isValid()) {
|
|
abort();
|
|
return;
|
|
}
|
|
|
|
if (!isGreetingMessageSent)
|
|
sendGreetingMessage();
|
|
|
|
pingTimer.start();
|
|
pongTime.start();
|
|
state = ReadyForUse;
|
|
emit readyForUse();
|
|
}
|
|
|
|
void Connection::processData()
|
|
{
|
|
switch (currentDataType) {
|
|
case PlainText:
|
|
emit newMessage(username, buffer);
|
|
break;
|
|
case Ping:
|
|
writer.startMap(1);
|
|
writer.append(Pong);
|
|
writer.append(nullptr); // no payload
|
|
writer.endMap();
|
|
break;
|
|
case Pong:
|
|
pongTime.restart();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
currentDataType = Undefined;
|
|
buffer.clear();
|
|
}
|