mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions Convert raw millisecond integer literals at time-duration call sites to `std::chrono` literals, and add `#include <chrono>` to each touched translation unit. Examples: - `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)` - `mpTimerReplay->setInterval(1000)` → `setInterval(1s)` - `mPendingTimer.start(60000)` → `start(1min)` - `QObject::startTimer(50)` → `startTimer(50ms)` - `QTest::qWait(100)` → `QTest::qWait(100ms)` - `QThread::msleep(10)` → `QThread::sleep(10ms)` This is a semantics-preserving refactor - every duration is kept exactly equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes `1min`). No behavioural change. #### Motivation for adding to Mudlet Chrono literals make time durations self-documenting and type-safe. `1s` / `100ms` read unambiguously where a bare `1000` / `100` forces the reader to remember each API's unit, and the compiler now rejects unit mismatches. Only genuine duration arguments were converted - loop counts, scroll-line counts, sizes, ports and the like were deliberately left as plain integers. All targeted APIs provide `std::chrono` overloads in the minimum supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8), `QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)` (6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7). #### Other info (issues closed, discussion etc) Test case: the full application builds cleanly and the entire functional `ctest` suite passes. The only failing test is the known, pre-existing `PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is unrelated to this change. Assisted-by: Claude:claude-opus-4-8
1149 lines
38 KiB
C++
1149 lines
38 KiB
C++
/***************************************************************************
|
||
* Copyright (C) 2024 by John McKisson - john.mckisson@gmail.com *
|
||
* Copyright (C) 2024, 2026 by Stephen Lyons - slysven@virginmedia.com *
|
||
* *
|
||
* This program is free software; you can redistribute it and/or modify *
|
||
* it under the terms of the GNU General Public License as published by *
|
||
* the Free Software Foundation; either version 2 of the License, or *
|
||
* (at your option) any later version. *
|
||
* *
|
||
* This program 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 General Public License for more details. *
|
||
* *
|
||
* You should have received a copy of the GNU General Public License *
|
||
* along with this program; if not, write to the *
|
||
* Free Software Foundation, Inc., *
|
||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||
***************************************************************************/
|
||
|
||
#include "MMCPClient.h"
|
||
#include "Host.h"
|
||
#include "MMCPServer.h"
|
||
#include "TEvent.h"
|
||
#include "mudlet.h"
|
||
|
||
#include <QHostAddress>
|
||
#include <QTcpSocket>
|
||
#include <QRegularExpression>
|
||
#include <QtGlobal>
|
||
#include <chrono>
|
||
|
||
using namespace std::chrono_literals;
|
||
|
||
QString convertToIPv4(QHostAddress addr)
|
||
{
|
||
// Check if the address is an IPv4-mapped IPv6 address
|
||
if (addr.protocol() == QAbstractSocket::IPv6Protocol && addr.isInSubnet(QHostAddress::parseSubnet("::ffff:0:0/96"))) {
|
||
// Convert to IPv4
|
||
QHostAddress ipv4addr = QHostAddress(addr.toIPv4Address());
|
||
return ipv4addr.toString();
|
||
}
|
||
// Return the original address if it's not an IPv4-mapped IPv6 address
|
||
// or conversion is not applicable
|
||
return addr.toString();
|
||
}
|
||
|
||
MMCPClient::MMCPClient(Host* pHost, MMCPServer* pServer)
|
||
: mpHost(pHost)
|
||
, mpMMCPServer(pServer)
|
||
, mTcpSocket(this)
|
||
, mLastColorBold(false)
|
||
, mNeedsColorTracking(false)
|
||
, mNeedsColorSkip(false)
|
||
{
|
||
//Disable Nagle's algorithm
|
||
mTcpSocket.setSocketOption(QAbstractSocket::LowDelayOption, 1);
|
||
|
||
connect(&mTcpSocket, &QTcpSocket::connected, this, &MMCPClient::slot_connected);
|
||
connect(&mTcpSocket, &QTcpSocket::disconnected, this, &MMCPClient::slot_disconnected);
|
||
connect(&mTcpSocket, &QTcpSocket::readyRead, this, &MMCPClient::slot_readData);
|
||
connect(&mTcpSocket, &QAbstractSocket::errorOccurred, this, &MMCPClient::slot_displayError);
|
||
connect(this, &MMCPClient::signal_clientDisconnected, mpMMCPServer, &MMCPServer::slot_clientDisconnected);
|
||
|
||
// Setup pending connection timeout
|
||
mPendingTimer.setSingleShot(true);
|
||
connect(&mPendingTimer, &QTimer::timeout, this, &MMCPClient::slot_pendingTimeout);
|
||
}
|
||
|
||
/**
|
||
* Attempt an outgoing connection to a client
|
||
*/
|
||
void MMCPClient::tryConnect(const QString& host, quint16 port)
|
||
{
|
||
mState = ConnectingOut;
|
||
mTcpSocket.connectToHost(host, port);
|
||
}
|
||
|
||
|
||
/**
|
||
* Disconnect the client. This will result in the disconnected() signal.
|
||
*/
|
||
void MMCPClient::disconnect()
|
||
{
|
||
mTcpSocket.disconnectFromHost();
|
||
}
|
||
|
||
|
||
/**
|
||
* Handle an incoming chat connection.
|
||
*/
|
||
bool MMCPClient::incoming(qintptr socketDesc)
|
||
{
|
||
mState = ConnectingIn;
|
||
return mTcpSocket.setSocketDescriptor(socketDesc);
|
||
}
|
||
|
||
QString MMCPClient::host()
|
||
{
|
||
return convertToIPv4(mTcpSocket.peerAddress());
|
||
}
|
||
|
||
quint16 MMCPClient::port()
|
||
{
|
||
return mTcpSocket.peerPort();
|
||
}
|
||
|
||
|
||
/**
|
||
* Outgoing connection established, send chat request.
|
||
*/
|
||
void MMCPClient::slot_connected()
|
||
{
|
||
mPeerAddress = convertToIPv4(mTcpSocket.peerAddress());
|
||
mPeerPort = mTcpSocket.peerPort();
|
||
|
||
QString str = qsl("CHAT:%1\n%2%3")
|
||
.arg(mpMMCPServer->getChatName(),
|
||
mPeerAddress)
|
||
.arg(mPeerPort, -5);
|
||
|
||
mTcpSocket.write(str.toLatin1());
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Waiting for response from %1:%2...").arg(mPeerAddress, QString::number(mPeerPort));
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
|
||
/**
|
||
* A client has been disconected.
|
||
* Clear snoop flags if we were snooping or they were snooping us.
|
||
*/
|
||
void MMCPClient::slot_disconnected()
|
||
{
|
||
// Stop pending timer if it's running
|
||
if (mPendingTimer.isActive()) {
|
||
mPendingTimer.stop();
|
||
}
|
||
|
||
/*: This message is used when a MMCP peer without a name disconnects,
|
||
* %1 is the peer's IP address (numbers or URL), %2 is the port they are
|
||
* listening on. Should be similiar to the one when we do have a name.
|
||
*/
|
||
const QString infoMsg = mPeerName.isEmpty() ? tr("[ CHAT ] - You are now disconnected from <unknown> - %1:%2.").arg(mPeerAddress, QString::number(mPeerPort))
|
||
/*: This message is used when a MMCP peer with a name disconnects,
|
||
* %1 is the peer's name, %2 is the peer's IP address (numbers or URL),
|
||
* %3 is the port they are listening on. Should be similiar to the one when
|
||
* we do not have a name.
|
||
*/
|
||
: tr("[ CHAT ] - You are now disconnected from %1 - %2:%3.").arg(mPeerName, mPeerAddress, QString::number(mPeerPort));
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
if (isSnooping()) {
|
||
setSnooping(false);
|
||
mpMMCPServer->decrementSnoopCount();
|
||
}
|
||
|
||
if (isSnooped()) {
|
||
setSnooped(false);
|
||
}
|
||
|
||
emit signal_clientDisconnected(this);
|
||
}
|
||
|
||
/**
|
||
* Pending connection was never accepted or denied by us.
|
||
* Notify the user and disconnect.
|
||
*/
|
||
void MMCPClient::slot_pendingTimeout()
|
||
{
|
||
if (mState == Pending) {
|
||
const QString infoMsg = tr("[ CHAT ] - Connection from %1 at %2:%3 timed out (not accepted or denied by you).")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
mState = Disconnected;
|
||
writeData(qsl("NO:%1\n").arg(mpMMCPServer->getChatName()));
|
||
disconnect();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Process incoming data from the client socket
|
||
*/
|
||
void MMCPClient::slot_readData()
|
||
{
|
||
while (mTcpSocket.bytesAvailable()) {
|
||
if (mPeerBuffer.length() > 0) {
|
||
qDebug().noquote().nospace() << "MMCPClient::slot_readData() INFO - appending to previous partial buffer.";
|
||
}
|
||
|
||
mPeerBuffer.append(mTcpSocket.read(mTcpSocket.bytesAvailable()));
|
||
}
|
||
|
||
switch (mState) {
|
||
case ConnectingIn: {
|
||
// Another peer is asking to connect to us and we need to reply
|
||
// appropriately:
|
||
|
||
const int nlPos = mPeerBuffer.indexOf('\n');
|
||
// As mPeerBuffer is a QByteArray we do not need to "wrap" a const char*
|
||
// to test against it:
|
||
if (!mPeerBuffer.startsWith("CHAT:") || nlPos == -1) {
|
||
mState = Disconnected;
|
||
disconnect();
|
||
return;
|
||
}
|
||
|
||
const QByteArray peerName = mPeerBuffer.mid(5, nlPos - 5);
|
||
|
||
// The spec really doesnt dictate a max length, but we should have one to avoid abuse
|
||
if (peerName.length() > 64) {
|
||
qWarning() << "MMCPClient::slot_readData() - Rejecting connection: peer name too long (" << peerName.length() << " bytes)";
|
||
mState = Disconnected;
|
||
writeData(qsl("NO:%1\n").arg(mpMMCPServer->getChatName()));
|
||
disconnect();
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Connection from %1 at %2:%3 denied (Peer name too long (64 chars max)).")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
return;
|
||
}
|
||
|
||
const QByteArray ipAndPort = mPeerBuffer.mid(nlPos + 1);
|
||
|
||
// Validate that we have enough data for IP and port (at least 5 chars for port)
|
||
if (ipAndPort.size() < 5) {
|
||
qWarning() << "MMCPClient::slot_readData() - Malformed handshake: insufficient data for IP and port";
|
||
mState = Disconnected;
|
||
disconnect();
|
||
return;
|
||
}
|
||
|
||
// Exclude the last 5 characters for the IP address
|
||
const QByteArray ipAddress = ipAndPort.left(ipAndPort.size() - 5);
|
||
// Last 5 characters for the port
|
||
const QByteArray port = ipAndPort.right(5);
|
||
|
||
QHostAddress host(QString::fromUtf8(ipAddress));
|
||
|
||
if (mpMMCPServer->isDoNotDisturb()) {
|
||
mState = Disconnected;
|
||
writeData(qsl("NO:%1\n").arg(mpMMCPServer->getChatName()));
|
||
disconnect();
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Connection from %1 at %2:%3 denied (DoNotDisturb).")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
|
||
mpHost->postMessage(infoMsg);
|
||
} else {
|
||
|
||
mPeerName = QString::fromUtf8(peerName);
|
||
|
||
// The ipAddress string to QHostAddress may have failed
|
||
mPeerAddress = host.isNull() ? "<Unknown>" : convertToIPv4(host);
|
||
bool ok;
|
||
mPeerPort = QString::fromUtf8(port).toUInt(&ok);
|
||
if (!ok) {
|
||
mState = Disconnected;
|
||
disconnect();
|
||
} else {
|
||
|
||
quint16 clientId = mpMMCPServer->addConnectedClient(this);
|
||
|
||
if (mpHost->getMMCPAutoAcceptCalls()) {
|
||
acceptCall();
|
||
|
||
} else {
|
||
mState = Pending;
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Connection from %1 at %2:%3 is pending, use mmcp.accept(%4) or mmcp.deny(%4) to accept or deny.")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()))
|
||
.arg(clientId);
|
||
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
// Start 60 second timeout for pending connection
|
||
// We don't know what client the peer is using, their client may timeout the socket, or may not
|
||
// Regardless, we'll time it out after 60 seconds
|
||
mPendingTimer.start(1min);
|
||
}
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case ConnectingOut: {
|
||
// We have requested to another peer and they should be accepting or
|
||
// rejecting our attempt with either a YES or a NO:
|
||
const int colonPos = mPeerBuffer.indexOf(':');
|
||
const int nlPos = mPeerBuffer.indexOf('\n');
|
||
|
||
bool isNo = mPeerBuffer.startsWith("NO:");
|
||
|
||
if (!(mPeerBuffer.startsWith("YES:") || isNo) || nlPos == -1 || colonPos == -1) {
|
||
mState = Disconnected;
|
||
|
||
// In this case we do not get details of the connection from the
|
||
// other end - instead we can only report the apparent details from
|
||
// the socket:
|
||
const QString infoMsg = tr("[ CHAT ] - Connection to %1:%2 refused.")
|
||
.arg(convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
} else if (isNo) {
|
||
// We were rejected by the other end, but we do get details of the connection from them, so we can report that:
|
||
mState = Disconnected;
|
||
const QByteArray peerName = mPeerBuffer.mid(colonPos + 1, nlPos - colonPos - 1);
|
||
const QString rejectedBy = QString::fromUtf8(peerName);
|
||
const QString infoMsg = tr("[ CHAT ] - Connection to %1 at %2:%3 rejected.")
|
||
.arg(rejectedBy,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
} else {
|
||
// We have got a "YES:" - yippee!
|
||
const QByteArray peerName = mPeerBuffer.mid(colonPos + 1, nlPos - colonPos - 1);
|
||
const QByteArray cmdData = mPeerBuffer.right(mPeerBuffer.size() - nlPos - 1);
|
||
|
||
mPeerName = QString::fromUtf8(peerName);
|
||
mState = Connected;
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Connection to %1 at %2:%3 accepted.")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
|
||
mpHost->postMessage(infoMsg);
|
||
mpMMCPServer->addConnectedClient(this);
|
||
sendVersion();
|
||
|
||
if (cmdData.length() > 0) {
|
||
qDebug().noquote().nospace() << "MMCPClient::slot_readData() INFO - additional cmd at connect: \"" << cmdData << "\".";
|
||
//A command was tacked on the end of the accept string
|
||
handleConnectedState(cmdData);
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case Connected:
|
||
if (!mPeerBuffer.endsWith(static_cast<char>(0xff))) {
|
||
qDebug().noquote().nospace() << "MMCPClient::slot_readData() INFO - partial buffer received, waiting for the rest...";
|
||
return;
|
||
}
|
||
|
||
if (!handleConnectedState(mPeerBuffer)) {
|
||
// We had a partial command in the buffer, wait for more data before clearing the buffer
|
||
return;
|
||
}
|
||
break;
|
||
|
||
case Pending:
|
||
// We got something from a pending peer, dont do anything with it
|
||
break;
|
||
|
||
default:
|
||
qDebug("unknown client state");
|
||
}
|
||
|
||
mPeerBuffer.clear();
|
||
}
|
||
|
||
/**
|
||
* Accepts an incoming chat connection
|
||
* Set's the client state to Connected, sends the success handshake response
|
||
* and our client version to the new peer
|
||
*/
|
||
void MMCPClient::acceptCall() {
|
||
mPendingTimer.stop(); // Stop timeout
|
||
mState = Connected;
|
||
writeData(QString("YES:%1\n").arg(mpMMCPServer->getChatName()));
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Connection from %1 at %2:%3 accepted.")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
sendVersion();
|
||
}
|
||
|
||
/**
|
||
* Denies an incoming chat connection
|
||
*
|
||
*/
|
||
void MMCPClient::denyCall() {
|
||
mPendingTimer.stop(); // Stop timeout
|
||
mState = Disconnected;
|
||
|
||
writeData(qsl("NO:%1\n").arg(mpMMCPServer->getChatName()));
|
||
|
||
disconnect();
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Connection from %1 at %2:%3 denied.")
|
||
.arg(mPeerName,
|
||
convertToIPv4(mTcpSocket.peerAddress()),
|
||
QString::number(mTcpSocket.peerPort()));
|
||
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
|
||
|
||
void MMCPClient::slot_displayError(QAbstractSocket::SocketError socketError)
|
||
{
|
||
QString message;
|
||
|
||
switch (socketError) {
|
||
case QAbstractSocket::RemoteHostClosedError:
|
||
// This will be hit if a TinTin++ client is in DND mode and is not
|
||
// accepting connections.
|
||
message = tr("[ CHAT ] - The peer closed or refused the connection.");
|
||
break;
|
||
case QAbstractSocket::HostNotFoundError:
|
||
message = tr("[ CHAT ] - The peer was not found. Please check the host name and port settings.");
|
||
break;
|
||
case QAbstractSocket::ConnectionRefusedError:
|
||
message = tr("[ CHAT ] - The connection was refused by the peer.");
|
||
break;
|
||
default:
|
||
message = tr("[ CHAT ] - The following error occurred: %1.").arg(mTcpSocket.errorString());
|
||
}
|
||
|
||
mpHost->postMessage(message);
|
||
|
||
// If we failed to connect, we are not yet in the peers list and need to clean up
|
||
if (mState == ConnectingOut) {
|
||
deleteLater();
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Send a Message to the client socket
|
||
*/
|
||
void MMCPClient::sendMessage(const QString& msg)
|
||
{
|
||
QByteArray out;
|
||
out.append(static_cast<char>(Message));
|
||
out.append(msg.toLatin1());
|
||
out.append(static_cast<char>(End));
|
||
writeData(out);
|
||
}
|
||
|
||
|
||
/**
|
||
* Send a ping request to this client.
|
||
*/
|
||
void MMCPClient::sendPingRequest()
|
||
{
|
||
QByteArray pingData;
|
||
pingData.append(static_cast<char>(PingRequest));
|
||
pingData.append(QByteArray::number(QDateTime::currentMSecsSinceEpoch()));
|
||
pingData.append(static_cast<char>(End));
|
||
writeData(pingData);
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Pinging %1...").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
|
||
/**
|
||
* Send a peek connections request to this client.
|
||
*/
|
||
void MMCPClient::sendPeekRequest()
|
||
{
|
||
QByteArray peekData;
|
||
peekData.append(static_cast<char>(PeekConnections));
|
||
peekData.append(static_cast<char>(End));
|
||
writeData(peekData);
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Attempting to peek at %1's public connections...").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
|
||
/**
|
||
* Request client connections list
|
||
*/
|
||
void MMCPClient::sendRequestConnections()
|
||
{
|
||
QByteArray requestData;
|
||
requestData.append(static_cast<char>(RequestConnections));
|
||
requestData.append(static_cast<char>(End));
|
||
writeData(requestData);
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - Requested connections from %1").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
|
||
|
||
/**
|
||
* Send client version
|
||
*/
|
||
void MMCPClient::sendVersion()
|
||
{
|
||
QByteArray versionData;
|
||
versionData.append(static_cast<char>(Version));
|
||
versionData.append(mudlet::self()->scmVersion.toLatin1());
|
||
versionData.append(static_cast<char>(End));
|
||
writeData(versionData);
|
||
}
|
||
|
||
/**
|
||
* Assign a client to a group.
|
||
* If supplied group is "none", "<none" or empty string, remove from group.
|
||
* Returns true if they were assigned to a group, false if they were removed.
|
||
*/
|
||
bool MMCPClient::setGroup(const QString& group)
|
||
{
|
||
if (group.isEmpty() || !group.compare(qsl("none"), Qt::CaseInsensitive) || !group.compare(csDefaultMMCPGroupName, Qt::CaseInsensitive)) {
|
||
mGroup = csDefaultMMCPGroupName;
|
||
return false;
|
||
}
|
||
|
||
mGroup = group;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Write to this client socket.
|
||
*/
|
||
void MMCPClient::writeData(const QString& data)
|
||
{
|
||
qint64 bytesWritten = mTcpSocket.write(data.toLatin1());
|
||
if (bytesWritten <= 0) {
|
||
const QString identifier = mPeerName.isEmpty() ? convertToIPv4(mTcpSocket.peerAddress()) : mPeerName;
|
||
if (bytesWritten == 0) {
|
||
qWarning() << "MMCPClient::writeData(QString&) Failed to write data to socket for client " << identifier;
|
||
} else if (bytesWritten == -1) {
|
||
qWarning() << "MMCPClient::writeData(QString&) - Failed to write data to socket:" << mTcpSocket.errorString() << " for client " << identifier;
|
||
}
|
||
}
|
||
}
|
||
|
||
void MMCPClient::writeData(const QByteArray& data)
|
||
{
|
||
qint64 bytesWritten = mTcpSocket.write(data);
|
||
if (bytesWritten <= 0) {
|
||
const QString identifier = mPeerName.isEmpty() ? convertToIPv4(mTcpSocket.peerAddress()) : mPeerName;
|
||
if (bytesWritten == 0) {
|
||
qWarning() << "MMCPClient::writeData(QByteArray&) Failed to write data to socket for client " << identifier;
|
||
} else if (bytesWritten == -1) {
|
||
qWarning() << "MMCPClient::writeData(QByteArray&) - Failed to write data to socket:" << mTcpSocket.errorString() << " for client " << identifier;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Attempt to snoop this client.
|
||
*/
|
||
void MMCPClient::snoop()
|
||
{
|
||
QByteArray snoopCmd;
|
||
snoopCmd.append(static_cast<char>(Snoop));
|
||
snoopCmd.append(static_cast<char>(End));
|
||
writeData(snoopCmd);
|
||
}
|
||
|
||
/**
|
||
* Process incoming data from this client socket.
|
||
* Find and handle chat commands.
|
||
* @return True if we completely processed the buffer
|
||
* @return False if we encountered a partial command need to wait for additional data
|
||
*/
|
||
bool MMCPClient::handleConnectedState(const QByteArray& bytes)
|
||
{
|
||
const char* data = bytes.data();
|
||
int cmdIdx = 0;
|
||
while (cmdIdx < bytes.length()) {
|
||
const char cmd = data[cmdIdx];
|
||
|
||
const int endIdx = bytes.indexOf(MMCPChatCommand::End, cmdIdx);
|
||
if (endIdx == -1) {
|
||
qDebug().noquote().nospace() << "MMCPClient::handleConnectedState(...) INFO - partial buffer detected...";
|
||
return false;
|
||
}
|
||
|
||
const QString stringData = QString::fromLatin1(data + cmdIdx + 1, (endIdx - cmdIdx) - 1);
|
||
|
||
switch (cmd) {
|
||
case MMCPChatCommand::NameChange:
|
||
handleIncomingNameChange(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::RequestConnections:
|
||
handleIncomingConnectionsRequest();
|
||
break;
|
||
|
||
case MMCPChatCommand::ConnectionList:
|
||
handleIncomingConnectionList(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::TextEveryone:
|
||
handleIncomingChatEveryone(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::TextPersonal:
|
||
handleIncomingChatPersonal(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::TextGroup:
|
||
handleIncomingChatGroup(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::Message:
|
||
mpMMCPServer->clientMessage(this, stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::Version:
|
||
handleIncomingClientVersion(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::PingRequest:
|
||
handleIncomingPingRequest(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::PingResponse:
|
||
handleIncomingPingResponse(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::PeekConnections:
|
||
handleIncomingPeekConnections();
|
||
break;
|
||
|
||
case MMCPChatCommand::PeekList:
|
||
handleIncomingPeekList(stringData);
|
||
break;
|
||
|
||
case MMCPChatCommand::Snoop:
|
||
handleIncomingSnoop();
|
||
break;
|
||
|
||
case MMCPChatCommand::SnoopData:
|
||
handleIncomingSnoopData(data + cmdIdx + 1, (endIdx - cmdIdx) - 1);
|
||
break;
|
||
|
||
case MMCPChatCommand::SideChannel:
|
||
handleIncomingSideChannelData(stringData);
|
||
break;
|
||
|
||
default:
|
||
qDebug().noquote().nospace() << "MMCPClient::handleConnectedState(...) INFO - unknown command: 0x" << QString::number(static_cast<char>(cmd), 16).toUpper() << ": \"" << stringData << "\".";
|
||
}
|
||
|
||
cmdIdx = endIdx + 1;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
void MMCPClient::handleIncomingClientVersion(const QString& version) {
|
||
mPeerVersion = version;
|
||
|
||
bool isMudMaster = mPeerVersion.contains("MudMaster");
|
||
bool isMudlet = mPeerVersion.contains("Mudlet");
|
||
bool isMUSHClient = mPeerVersion.contains("MUSHc");
|
||
bool isTinTin = mPeerVersion.contains("TinTin");
|
||
|
||
mNeedsColorTracking = isMudMaster || isMudlet || isMUSHClient || isTinTin;
|
||
|
||
mNeedsColorSkip = isMudMaster || isMUSHClient;
|
||
}
|
||
|
||
/**
|
||
* We were sent a connection list, we're supposed to connect to the clients
|
||
* given to us
|
||
* Maybe check option if we want to auto call everyone in this list
|
||
*/
|
||
void MMCPClient::handleIncomingConnectionList(const QString& list)
|
||
{
|
||
QStringList parts = list.split(',');
|
||
|
||
if (parts.size() % 2 != 0) {
|
||
mpHost->postMessage(tr("[ CHAT ] - Badly formatted connection list from %1")
|
||
.arg(mPeerName));
|
||
return;
|
||
}
|
||
for (int i = 0; i < parts.size(); i++) {
|
||
const QString host = parts[i];
|
||
bool ok = false;
|
||
uint16_t port = parts[++i].toUInt(&ok);
|
||
|
||
if (!ok) {
|
||
mpHost->postMessage(tr("[ CHAT ] - Error parsing host value from connection: %1")
|
||
.arg(parts[i]));
|
||
continue;
|
||
}
|
||
|
||
mpHost->postMessage(tr("[ CHAT ] - Attempting to connect to %1:%2 provided by %3")
|
||
.arg(host).arg(port).arg(mPeerName));
|
||
|
||
mpMMCPServer->call(host, port);
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Someone requested that we give them our public connections
|
||
*/
|
||
void MMCPClient::handleIncomingConnectionsRequest()
|
||
{
|
||
if (mIgnored) {
|
||
const QString infoMsg = tr("[ CHAT ] - %1 is trying to request your connections!").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
return;
|
||
}
|
||
|
||
if (mpHost->getMMCPAllowPeekRequests()) {
|
||
const QString infoMsg = tr("[ CHAT ] - %1 has requested your public connections...").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
mpMMCPServer->sendPublicConnections(this);
|
||
} else {
|
||
const QString infoMsg = tr("[ CHAT ] - %1 has requested your public connections, but you're ignoring connection requests...").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display a chat all message and echo it to any clients we may be serving
|
||
* Or, if we're serving someone forward it to them
|
||
*/
|
||
void MMCPClient::handleIncomingChatEveryone(const QString& msg)
|
||
{
|
||
// We're ignoring chats from this person
|
||
if (mIgnored) {
|
||
return;
|
||
}
|
||
|
||
mpMMCPServer->clientMessage(this, msg);
|
||
|
||
// If mServed is true:
|
||
// We are serving this client, forward their message to everyone
|
||
// except them and ourself
|
||
// If mServed is false:
|
||
// Echo message to other clients we might be serving
|
||
|
||
// sendServedMessage expects an onlyToServed boolean
|
||
mpMMCPServer->sendServedMessage(this, msg, !mServed);
|
||
}
|
||
|
||
/**
|
||
* Display an incoming personal chat
|
||
*/
|
||
void MMCPClient::handleIncomingChatPersonal(const QString& msg)
|
||
{
|
||
mpMMCPServer->clientMessage(this, msg);
|
||
}
|
||
|
||
/**
|
||
* Display an incoming chat to a group
|
||
*/
|
||
void MMCPClient::handleIncomingChatGroup(const QString& msg)
|
||
{
|
||
const QString groupStr = msg.left(15).trimmed();
|
||
const QString trimmedMsg = msg.right(msg.length() - 15);
|
||
|
||
using namespace AnsiColors;
|
||
|
||
//: Incoming group message, %1, %2 and %4 are ANSI Escape codes
|
||
const QString groupMsg = tr("%1%2%3%4(%5)%1%2%6%1")
|
||
.arg(RST,
|
||
FBLDRED,
|
||
mpHost->getMMCPChatPrefix(),
|
||
FBLDCYN,
|
||
groupStr,
|
||
trimmedMsg);
|
||
|
||
mpMMCPServer->clientMessage(this, groupMsg);
|
||
}
|
||
|
||
/**
|
||
* A client changed their name
|
||
*/
|
||
void MMCPClient::handleIncomingNameChange(const QString& newName)
|
||
{
|
||
const QString infoMsg = tr("[ CHAT ] - %1 is now known as %2.").arg(mPeerName, newName);
|
||
mpHost->postMessage(infoMsg);
|
||
mPeerName = newName;
|
||
}
|
||
|
||
/**
|
||
* Someone has requested to peek our connections
|
||
*/
|
||
void MMCPClient::handleIncomingPeekConnections()
|
||
{
|
||
//check if this client is ignored before doing anything drastic
|
||
if (mIgnored) {
|
||
const QString infoMsg = tr("[ CHAT ] - %1 is trying to peek your connections!").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
return;
|
||
}
|
||
|
||
if (mpHost->getMMCPAllowPeekRequests()) {
|
||
const QString infoMsg = tr("[ CHAT ] - %1 is peeking at your connections...").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
|
||
mpMMCPServer->sendPublicPeek(this);
|
||
} else {
|
||
const QString infoMsg = tr("[ CHAT ] - %1 is trying to peek your connections, but you're ignoring peek requests...").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display someones peek list that we've requested
|
||
*/
|
||
void MMCPClient::handleIncomingPeekList(const QString& list)
|
||
{
|
||
// Drop the trailing ~ if it exists so modulus will do the trick after
|
||
// a split
|
||
QString str = list;
|
||
if (str.endsWith("~")) {
|
||
str.chop(1);
|
||
}
|
||
const QStringList parts = str.split("~");
|
||
if (parts.size() % 3 != 0) {
|
||
const QString infoMsg = tr("[ CHAT ] - Badly formatted peek list from %1.").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
return;
|
||
}
|
||
|
||
QStringList messageList;
|
||
using namespace AnsiColors;
|
||
|
||
quint16 count = 0;
|
||
for (int i = 0; (i + 1) < parts.size(); i += 3) {
|
||
messageList << qsl("%1%2 %3%4 %5 %6")
|
||
.arg(FBLDWHT)
|
||
.arg(++count, 4)
|
||
.arg(RST)
|
||
.arg(parts.at(i + 2), -20) // Peer name
|
||
.arg(parts.at(i), -15) // Peer address
|
||
.arg(parts.at(i + 1), 5); // Peer port
|
||
}
|
||
|
||
// We need to include ID here as otherwise any leading spaces get trimmed
|
||
// off inside MMCPServer::clientMessage(...)!
|
||
const QString listOut = tr("Id Name Address Port\n"
|
||
"==== ==================== =============== =====\n"
|
||
"%1\n"
|
||
"%2==== ==================== =============== =====%3\n")
|
||
.arg(messageList.join(QChar::LineFeed), FBLDRED, RST);
|
||
mpMMCPServer->clientMessage(this, listOut);
|
||
}
|
||
|
||
/**
|
||
* Respond to someones ping request
|
||
*/
|
||
void MMCPClient::handleIncomingPingRequest(const QString& msg)
|
||
{
|
||
QByteArray out;
|
||
out.append(static_cast<char>(PingResponse));
|
||
out.append(msg.toLatin1());
|
||
out.append(static_cast<char>(End));
|
||
writeData(out);
|
||
}
|
||
|
||
/**
|
||
* Handle ping data that we've requested
|
||
*/
|
||
void MMCPClient::handleIncomingPingResponse(const QString& data)
|
||
{
|
||
bool ok;
|
||
qint64 returnTime = data.toLongLong(&ok);
|
||
|
||
if (ok) {
|
||
const QString infoMsg = tr("[ CHAT ] - Ping returned from %1: %2 ms").arg(mPeerName).arg(QDateTime::currentMSecsSinceEpoch() - returnTime);
|
||
mpHost->postMessage(infoMsg);
|
||
} else {
|
||
const QString infoMsg = tr("[ CHAT ] - Bad Ping response from %1: %2").arg(mPeerName, data);
|
||
mpHost->postMessage(infoMsg);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* This client sent a request to snoop you (the user).
|
||
* Apparently the client is responsible for telling the connecting user.
|
||
*
|
||
* We're sending a message back to the client to let them know if they
|
||
* can or cannot snoop us. We don't know what language the remote client is using...
|
||
* Send it in English for now and hope for the best? Maybe noone is using this snoop feature?
|
||
*/
|
||
void MMCPClient::handleIncomingSnoop()
|
||
{
|
||
if (!canSnoop()) {
|
||
sendMessage(qsl("<CHAT> You do not have permission to snoop %1.").arg(mpMMCPServer->getChatName()));
|
||
const QString infoMsg = tr("[ CHAT ] - %1 tried to snoop you but doesn't have permission.").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
return;
|
||
}
|
||
|
||
if (isSnooping()) {
|
||
setSnooping(false);
|
||
mpMMCPServer->decrementSnoopCount();
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - %1 has stopped snooping you.").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
sendMessage(qsl("<CHAT> You have stopped snooping %1.").arg(mpMMCPServer->getChatName()));
|
||
|
||
} else {
|
||
setSnooping(true);
|
||
mpMMCPServer->incrementSnoopCount();
|
||
|
||
const QString infoMsg = tr("[ CHAT ] - %1 has begun snooping you.").arg(mPeerName);
|
||
mpHost->postMessage(infoMsg);
|
||
sendMessage(qsl("<CHAT> You have begun snooping %1.").arg(mpMMCPServer->getChatName()));
|
||
}
|
||
}
|
||
|
||
void MMCPClient::updateSgrState(const std::string &ansiSeq)
|
||
{
|
||
// We expect SGR sequences of the form "\033[<codes>m"
|
||
if (ansiSeq.size() < 3 || ansiSeq.front() != '\033' || ansiSeq[1] != '[' || ansiSeq.back() != 'm')
|
||
return;
|
||
|
||
// Extract the inner code string (e.g., "1;35")
|
||
std::string codes = ansiSeq.substr(2, ansiSeq.size() - 3);
|
||
|
||
// If this is a full reset, clear our cumulative state.
|
||
if (codes == "0") {
|
||
mLastColorBold = false;
|
||
mLastSnoopColor = "";
|
||
} else {
|
||
// Tokenize the codes by ';'
|
||
std::istringstream iss(codes);
|
||
std::string token;
|
||
while (std::getline(iss, token, ';')) {
|
||
if (token == "1") {
|
||
mLastColorBold = true;
|
||
} else if (token == "22") { // Normal intensity – typically turns off bold.
|
||
mLastColorBold = false;
|
||
} else {
|
||
// Check for standard foreground color codes (30-37 or 90-97).
|
||
bool ok = false;
|
||
int val = QString::fromStdString(token).toInt(&ok);
|
||
if (ok && ((val >= 30 && val <= 37) || (val >= 90 && val <= 97))) {
|
||
mLastSnoopColor = token;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Reconstruct the cumulative ANSI sequence.
|
||
std::string newState = "\033[";
|
||
bool needSemicolon = false;
|
||
if (mLastColorBold) {
|
||
newState += "1";
|
||
needSemicolon = true;
|
||
}
|
||
if (!mLastSnoopColor.empty()) {
|
||
if (needSemicolon)
|
||
newState += ";";
|
||
newState += mLastSnoopColor;
|
||
}
|
||
newState += "m";
|
||
mLastSgrState = newState;
|
||
}
|
||
|
||
/**
|
||
* Handle remote client incoming snoop data.
|
||
*
|
||
* Client | needsSkip | needsTrack | snoop Mudlet | Mudlet can snoop
|
||
* Mudlet | no yes yes yes
|
||
* MudMaster | yes yes yes yes
|
||
* TinTin | ? yes no? yes
|
||
* MUSH | yes yes yes (color issue) yes
|
||
* Zmud | ? ? ? ?
|
||
* Cmud | ? ? ? ?
|
||
*
|
||
* mNeedsColorSkip and mNeedsColorTracking are defined when we receive
|
||
* client version in handleIncomingClientVersion
|
||
*
|
||
*/
|
||
void MMCPClient::handleIncomingSnoopData(const char* sData, quint16 len)
|
||
{
|
||
const char* inScan = sData;
|
||
const char* inEnd = inScan + len;
|
||
std::stringstream ss;
|
||
std::string outputMessage;
|
||
|
||
// If the remote client has prepended color information,
|
||
// skip over it as we'll be using our own.
|
||
// MudMaster seems to do this, other clients (TT++) may not
|
||
if (mNeedsColorSkip) {
|
||
if (len < 4) {
|
||
// Not enough data to skip color prefix - malformed packet?
|
||
qWarning() << "MMCPClient::handleIncomingSnoopData() - Insufficient data for color skip (len=" << len << ")";
|
||
return;
|
||
}
|
||
inScan += 4;
|
||
}
|
||
|
||
for (; inScan < inEnd; inScan++) {
|
||
char c = *inScan;
|
||
|
||
if (c == '\r') {
|
||
continue;
|
||
}
|
||
|
||
if (c == '\n') {
|
||
// End of a line: get the current line.
|
||
std::string line = ss.str();
|
||
|
||
// For clients like MudMaster that do not resend the color,
|
||
// if the line does not begin with an ANSI escape sequence,
|
||
// prepend the last known color sequence.
|
||
if (mNeedsColorTracking && !line.empty()) {
|
||
line = mLastSgrState + line;
|
||
}
|
||
|
||
// Append the processed line
|
||
outputMessage += line;
|
||
|
||
ss.str("");
|
||
ss.clear();
|
||
continue;
|
||
}
|
||
|
||
// Look for ANSI escapes
|
||
if (c == '\033') {
|
||
std::string ansiSeq;
|
||
ansiSeq.push_back(c);
|
||
|
||
if (inScan + 1 < inEnd) {
|
||
inScan++;
|
||
char nextChar = *inScan;
|
||
ansiSeq.push_back(nextChar);
|
||
|
||
if (nextChar == '[') {
|
||
|
||
// Read until we hit an m (yes while bad)
|
||
while (inScan + 1 < inEnd) {
|
||
inScan++;
|
||
char ch = *inScan;
|
||
ansiSeq.push_back(ch);
|
||
|
||
if (ch == 'm') {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
updateSgrState(ansiSeq);
|
||
|
||
// Append the full ANSI sequence to the current line
|
||
ss << ansiSeq;
|
||
continue;
|
||
}
|
||
|
||
ss << c;
|
||
}
|
||
|
||
// Process any remaining data that didn't end with a newline
|
||
std::string remaining = ss.str();
|
||
if (!remaining.empty() && mNeedsColorTracking) {
|
||
remaining = mLastSgrState + remaining;
|
||
}
|
||
|
||
outputMessage += remaining;
|
||
|
||
if (!outputMessage.empty()) {
|
||
|
||
postSnoopDataEvent(QString::fromStdString(outputMessage));
|
||
|
||
if (mpHost->getMMCPShowSnoopInMainConsole()) {
|
||
mpMMCPServer->snoopMessage(outputMessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
void MMCPClient::postSnoopDataEvent(const QString& message) {
|
||
TEvent event {};
|
||
event.mArgumentList << csMMCPIncomingSnoopEvent;
|
||
event.mArgumentList << mPeerName << message;
|
||
event.mArgumentTypeList << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING;
|
||
mpHost->raiseEvent(event);
|
||
}
|
||
|
||
void MMCPClient::postSideChannelMessage(const QString& from, const QString& channel, const QString& message)
|
||
{
|
||
TEvent event {};
|
||
event.mArgumentList << csMMCPChatSideChannelEvent;
|
||
event.mArgumentList << from << channel << message;
|
||
event.mArgumentTypeList << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING;
|
||
mpHost->raiseEvent(event);
|
||
}
|
||
|
||
/**
|
||
* Handle incoming side channel data from this client.
|
||
* Posts a sysMMCPSideChannelMessage event with arguments:
|
||
* chatname, channel, message
|
||
*/
|
||
void MMCPClient::handleIncomingSideChannelData(const QString& stringData)
|
||
{
|
||
QRegularExpression chatChannel("^(\\w+),(.*)$");
|
||
|
||
QRegularExpressionMatch match = chatChannel.match(stringData);
|
||
|
||
if (match.hasMatch()) {
|
||
postSideChannelMessage(mPeerName, match.captured(1), match.captured(2));
|
||
}
|
||
|
||
}
|
||
|
||
// To return exactly 8 ASCII characters:
|
||
const QString MMCPClient::getFlagsString()
|
||
{
|
||
return qsl("%1%2%3%4%5%6%7%8")
|
||
.arg(QLatin1Char(' '))
|
||
.arg(QLatin1Char(' '))
|
||
.arg(QLatin1Char(mPrivate ? 'P' : ' ')) // "Private"
|
||
.arg(QLatin1Char(mIgnored ? 'I' : ' ')) // "Ignored"
|
||
.arg(QLatin1Char(mServed ? 'S' : ' ')) // "Serving"
|
||
.arg(QLatin1Char((convertToIPv4(mTcpSocket.peerAddress()) != mPeerAddress) ? 'F' : ' ')) // "Firewall" - Humm?
|
||
.arg(QLatin1Char(mSnooping ? 'N' : (mEnableSnooping ? 'n' : ' ')))
|
||
.arg(QLatin1Char(' '));
|
||
}
|
||
|
||
const QString MMCPClient::getInfoString(int colName, int colAddr, int colPort, int colGroup, int colFlags)
|
||
{
|
||
using namespace AnsiColors;
|
||
|
||
const QString groupStr = (mGroup == csDefaultMMCPGroupName) ? QString(QChar::Space).repeated(colGroup) : mGroup;
|
||
|
||
return qsl("%1%6 %2 %3 %4 %5")
|
||
.arg(mPeerName, -colName)
|
||
.arg(convertToIPv4(mTcpSocket.peerAddress()), -colAddr)
|
||
.arg(mTcpSocket.peerPort(), colPort)
|
||
.arg(groupStr, -colGroup)
|
||
.arg(getFlagsString(), -colFlags)
|
||
.arg(RST);
|
||
}
|