Revision control

++ // Copyright (C) 2017 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #ifndef QMODBUSTCPCLIENT_P_H
++ #define QMODBUSTCPCLIENT_P_H
++
++ #include <QtCore/qloggingcategory.h>
++ #include <QtNetwork/qhostaddress.h>
++ #include <QtNetwork/qtcpsocket.h>
++ #include "QtSerialBus/qmodbustcpclient.h"
++
++ #include "private/qmodbus_symbols_p.h"
++ #include "private/qmodbusclient_p.h"
++
++ //
++ // W A R N I N G
++ // -------------
++ //
++ // This file is not part of the Qt API. It exists purely as an
++ // implementation detail. This header file may change from version to
++ // version without notice, or even be removed.
++ //
++ // We mean it.
++ //
++
++ QT_BEGIN_NAMESPACE
++
++ Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS)
++ Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW)
++
++ class QModbusTcpClientPrivate : public QModbusClientPrivate
++ {
++ Q_DECLARE_PUBLIC(QModbusTcpClient)
++
++ public:
++ void setupTcpSocket()
++ {
++ Q_Q(QModbusTcpClient);
++
++ m_socket = new QTcpSocket(q);
++
++ QObject::connect(m_socket, &QAbstractSocket::connected, q, [this]() {
++ qCDebug(QT_MODBUS) << "(TCP client) Connected to" << m_socket->peerAddress()
++ << "on port" << m_socket->peerPort();
++ Q_Q(QModbusTcpClient);
++ responseBuffer.clear();
++ q->setState(QModbusDevice::ConnectedState);
++ });
++
++ QObject::connect(m_socket, &QAbstractSocket::disconnected, q, [this]() {
++ qCDebug(QT_MODBUS) << "(TCP client) Connection closed.";
++ Q_Q(QModbusTcpClient);
++ q->setState(QModbusDevice::UnconnectedState);
++ cleanupTransactionStore();
++ });
++
++ QObject::connect(m_socket, &QAbstractSocket::errorOccurred, q,
++ [this](QAbstractSocket::SocketError /*error*/)
++ {
++ Q_Q(QModbusTcpClient);
++
++ if (m_socket->state() == QAbstractSocket::UnconnectedState) {
++ cleanupTransactionStore();
++ q->setState(QModbusDevice::UnconnectedState);
++ }
++ q->setError(QModbusClient::tr("TCP socket error (%1).").arg(m_socket->errorString()),
++ QModbusDevice::ConnectionError);
++ });
++
++ QObject::connect(m_socket, &QIODevice::readyRead, q, [this](){
++ responseBuffer += m_socket->read(m_socket->bytesAvailable());
++ qCDebug(QT_MODBUS_LOW) << "(TCP client) Response buffer:" << responseBuffer.toHex();
++
++ while (!responseBuffer.isEmpty()) {
++ // can we read enough for Modbus ADU header?
++ if (responseBuffer.size() < mbapHeaderSize) {
++ qCDebug(QT_MODBUS_LOW) << "(TCP client) MBPA header too short. Waiting for more data.";
++ return;
++ }
++
++ quint8 serverAddress;
++ quint16 transactionId, bytesPdu, protocolId;
++ QDataStream input(responseBuffer);
++ input >> transactionId >> protocolId >> bytesPdu >> serverAddress;
++
++ // stop the timer as soon as we know enough about the transaction
++ const bool knownTransaction = m_transactionStore.contains(transactionId);
++ if (knownTransaction && m_transactionStore[transactionId].timer)
++ m_transactionStore[transactionId].timer->stop();
++
++ qCDebug(QT_MODBUS) << "(TCP client) tid:" << Qt::hex << transactionId << "size:"
++ << bytesPdu << "server address:" << serverAddress;
++
++ const int tcpAduSize = mbapAduSize(bytesPdu);
++ if (!isValidAduSize(tcpAduSize)) {
++ // Resynchronizing would be guesswork, the next frame could start anywhere.
++ qCWarning(QT_MODBUS) << "(TCP client) Invalid MBAP length field:" << bytesPdu
++ << "closing connection.";
++ Q_Q(QModbusTcpClient);
++ m_socket->disconnectFromHost();
++ q->setError(QModbusTcpClient::tr("Received invalid MBAP length field."),
++ QModbusDevice::ProtocolError);
++ return;
++ }
++
++ if (responseBuffer.size() < tcpAduSize) {
++ qCDebug(QT_MODBUS) << "(TCP client) PDU too short. Waiting for more data";
++ return;
++ }
++
++ // Parse the PDU out of the declared frame only. QModbusPdu sizes its data
++ // from the function code and reads to the end of the device, so an
++ // unbounded stream lets it consume bytes belonging to the next frame.
++ QModbusResponse responsePdu;
++ QDataStream pdu(responseBuffer.sliced(mbapHeaderSize, tcpAduSize - mbapHeaderSize));
++ pdu >> responsePdu;
++ qCDebug(QT_MODBUS) << "(TCP client) Received PDU:" << responsePdu.functionCode()
++ << responsePdu.data().toHex();
++
++ responseBuffer.remove(0, tcpAduSize);
++
++ if (!knownTransaction) {
++ qCDebug(QT_MODBUS) << "(TCP client) No pending request for response with "
++ "given transaction ID, ignoring response message.";
++ } else {
++ processQueueElement(responsePdu, m_transactionStore[transactionId]);
++ }
++ }
++ });
++ }
++
++ QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress,
++ const QModbusDataUnit &unit,
++ QModbusReply::ReplyType type) override
++ {
++ auto writeToSocket = [this](quint16 tId, const QModbusRequest &request, int address) {
++ QByteArray buffer;
++ QDataStream output(&buffer, QIODevice::WriteOnly);
++ output << tId << quint16(0) << quint16(request.size() + 1) << quint8(address) << request;
++
++ int writtenBytes = m_socket->write(buffer);
++ if (writtenBytes == -1 || writtenBytes < buffer.size()) {
++ Q_Q(QModbusTcpClient);
++ qCDebug(QT_MODBUS) << "(TCP client) Cannot write request to socket.";
++ q->setError(QModbusTcpClient::tr("Could not write request to socket."),
++ QModbusDevice::WriteError);
++ return false;
++ }
++ qCDebug(QT_MODBUS_LOW) << "(TCP client) Sent TCP ADU:" << buffer.toHex();
++ qCDebug(QT_MODBUS) << "(TCP client) Sent TCP PDU:" << request << "with tId:" <<Qt:: hex
++ << tId;
++ return true;
++ };
++
++ const int tId = transactionId();
++ if (!writeToSocket(tId, request, serverAddress))
++ return nullptr;
++
++ Q_Q(QModbusTcpClient);
++ auto reply = new QModbusReply(type, serverAddress, q);
++ const auto element = QueueElement{ reply, request, unit, m_numberOfRetries,
++ m_responseTimeoutDuration };
++ m_transactionStore.insert(tId, element);
++
++ q->connect(reply, &QObject::destroyed, q, [this, tId](QObject *) {
++ if (!m_transactionStore.contains(tId))
++ return;
++ const QueueElement element = m_transactionStore.take(tId);
++ if (element.timer)
++ element.timer->stop();
++ });
++
++ if (element.timer) {
++ q->connect(q, &QModbusClient::timeoutChanged,
++ element.timer.data(), QOverload<int>::of(&QTimer::setInterval));
++ QObject::connect(element.timer.data(), &QTimer::timeout, q, [this, writeToSocket, tId]() {
++ if (!m_transactionStore.contains(tId))
++ return;
++
++ QueueElement elem = m_transactionStore.take(tId);
++ if (elem.reply.isNull())
++ return;
++
++ if (elem.numberOfRetries > 0) {
++ elem.numberOfRetries--;
++ if (!writeToSocket(tId, elem.requestPdu, elem.reply->serverAddress()))
++ return;
++ m_transactionStore.insert(tId, elem);
++ elem.timer->start();
++ qCDebug(QT_MODBUS) << "(TCP client) Resend request with tId:" << Qt::hex << tId;
++ } else {
++ qCDebug(QT_MODBUS) << "(TCP client) Timeout of request with tId:" <<Qt::hex << tId;
++ elem.reply->setError(QModbusDevice::TimeoutError,
++ QModbusClient::tr("Request timeout."));
++ }
++ });
++ element.timer->start();
++ } else {
++ qCWarning(QT_MODBUS) << "(TCP client) No response timeout timer for request with tId:"
++ << Qt::hex << tId << ". Expected timeout:" << m_responseTimeoutDuration;
++ }
++ incrementTransactionId();
++
++ return reply;
++ }
++
++ // TODO: Review once we have a transport layer in place.
++ bool isOpen() const override
++ {
++ if (m_socket)
++ return m_socket->isOpen();
++ return false;
++ }
++
++ void cleanupTransactionStore()
++ {
++ if (m_transactionStore.isEmpty())
++ return;
++
++ qCDebug(QT_MODBUS) << "(TCP client) Cleanup of pending requests";
++
++ for (const auto &elem : std::as_const(m_transactionStore)) {
++ if (elem.reply.isNull())
++ continue;
++ elem.reply->setError(QModbusDevice::ReplyAbortedError,
++ QModbusClient::tr("Reply aborted due to connection closure."));
++ }
++ m_transactionStore.clear();
++ }
++
++ // This doesn't overflow, it rather "wraps around". Expected.
++ inline void incrementTransactionId() { m_transactionId++; }
++ inline int transactionId() const { return m_transactionId; }
++
++ QIODevice *device() const override { return m_socket; }
++
++ QTcpSocket *m_socket = nullptr;
++ QByteArray responseBuffer;
++ QHash<quint16, QueueElement> m_transactionStore;
++
++ private: // Private to avoid using the wrong id inside the timer lambda,
++ quint16 m_transactionId = 0; // capturing 'this' will not copy the id.
++ };
++
++ QT_END_NAMESPACE
++
++ #endif // QMODBUSTCPCLIENT_P_H