diff --git a/README.md b/README.md index a871b41..5aee7e8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # nymea-mqtt Nymea MQTT broker -The nymea MQTT broker consists of a library containing a MqttClient and a MqttServer implementation. +The nymea MQTT broker consists of a Qt library containing a MqttClient and a MqttServer implementation. It can be used standalone or integrated in other applications. + +The currently supported MQTT protocol versions are 3.1.0 and 3.1.1. + +Both, the client and the server support raw MQTT over TCP as well as MQTT over web socket. Both transports +can be used with or without SSL encryption. + +Please refer to the server and client directories for minimalistic, yet fully featured examples on how to +use the library. diff --git a/client/client.pro b/client/client.pro new file mode 100644 index 0000000..b26048a --- /dev/null +++ b/client/client.pro @@ -0,0 +1,16 @@ +TEMPLATE = app +TARGET = nymea-mqtt-client + +include(../nymea-mqtt.pri) + +QT += network +QT -= gui + +INCLUDEPATH += $$top_srcdir/libnymea-mqtt/ + +SOURCES += main.cpp + +LIBS += -L$${top_builddir}/libnymea-mqtt -lnymea-mqtt + +target.path = $$[QT_INSTALL_PREFIX]/bin +INSTALLS += target diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..58d412f --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,178 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttclient.h" + +#include +#include +#include +#include + + +int main(int argc, char *argv[]) { + + QCoreApplication *app = new QCoreApplication(argc, argv); + + QCommandLineParser parser; + parser.addPositionalArgument("server", "The server address."); + parser.addOptions({ + {{"clientid", "c"}, "The client ID to use for the connection (default: autogenerated).", "clientId", QUuid::createUuid().toString()}, + {{"username", "u"}, "The user name to use for the connection.", "username", ""}, + {{"password", "P"}, "The password to use for the connection.", "password", ""}, + {{"subscribe", "s"}, "Subscribe to a topic filter.", "topicfilter"}, + {{"publish", "p"}, "Publish to topic.", "topic"}, + {{"payload", "l"}, "Publish payload (requires -p).", "payload"}, + {{"retain", "r"}, "Retain flag for publishes (default: no retaining)."}, + {{"qos", "q"}, "QoS (default: 1).", "QoS", "1"}, + {{"ssl", "S"}, "Use SSL for TCP connections (default: off) (Websocket connections will determine the use of SSL using the url scheme)."}, + {{"accept-self-signed-certificate", "A"}, "Ignore self signed certificate errors"}, + {{"verbose", "v"}, "Be more verbose"} + }); + parser.addHelpOption(); + + parser.process(app->arguments()); + + if (parser.positionalArguments().count() < 1) { + parser.showHelp(-1); + } + + QString verbose = parser.isSet("verbose") ? "true" : "false"; + QLoggingCategory::setFilterRules(QString("nymea.mqtt.client.debug=%1\nnymea.mqtt.client.warning=%2\n").arg(verbose).arg(verbose)); + + bool ok; + int qosInt = parser.value("qos").toInt(&ok); + if (!ok || qosInt < 0 || qosInt > 2) { + qCritical() << "Invalid QoS option. Options are 0, 1 or 2."; + exit(EXIT_FAILURE); + } + Mqtt::QoS qos = static_cast(qosInt); + + bool retain = parser.isSet("retain"); + + MqttClient client(parser.value("clientid"), app); + QObject::connect(&client, &MqttClient::error, app, [&client, app](QAbstractSocket::SocketError socketError){ + qCritical() << "Connection error:" << socketError; + client.setAutoReconnect(false); + app->quit(); + }); + QObject::connect(&client, &MqttClient::sslErrors, app, [&client, &parser](const QList &sslErrors){ + QSslCertificate certificate = sslErrors.first().certificate(); + if (parser.isSet("accept-self-signed-certificate")) { + QList copy = sslErrors; + copy.removeOne(QSslError(QSslError::HostNameMismatch, certificate)); + copy.removeAll(QSslError(QSslError::SelfSignedCertificate, certificate)); + if (copy.isEmpty()) { + qInfo() << "Accepting self signed certificate"; + client.ignoreSslErrors(); + return; + } + } + qWarning() << "SSL errors for certificate:" << qUtf8Printable(certificate.toText()) << sslErrors; + }); + QObject::connect(&client, &MqttClient::connected, app, [&parser, &client, qos, retain](Mqtt::ConnectReturnCode connectReturnCode, Mqtt::ConnackFlags /*connackFlags*/){ + switch (connectReturnCode) { + case Mqtt::ConnectReturnCodeIdentifierRejected: + qCritical() << "Connection failed: Identifier rejected"; + exit(EXIT_FAILURE); + case Mqtt::ConnectReturnCodeUnacceptableProtocolVersion: + qCritical() << "Connection failed: Unsupported MQTT protocol version"; + exit(EXIT_FAILURE); + case Mqtt::ConnectReturnCodeBadUsernameOrPassword: + qCritical() << "Connection failed: Bad username or password"; + exit(EXIT_FAILURE); + case Mqtt::ConnectReturnCodeNotAuthorized: + qCritical() << "Connection failed: Not authorized"; + exit(EXIT_FAILURE); + case Mqtt::ConnectReturnCodeServerUnavailable: + qCritical() << "Connection failed: Server unavailable"; + exit(EXIT_FAILURE); + default: + qInfo() << "Connected to server."; + } + + foreach (const QString &topicFilter, parser.values("subscribe")) { + qDebug() << "Subscribing to" << topicFilter; + client.subscribe(topicFilter, qos); + } + + for (int i = 0; i < parser.values("publish").length(); i++) { + QString topic = parser.values("publish").at(0); + QByteArray payload; + if (parser.values("payload").length() > i) { + payload = parser.values("payload").at(i).toUtf8(); + } + qDebug().nospace() << "Publishing to " << topic << (!payload.isEmpty() ? ": " + payload : ""); + client.publish(parser.value("publish"), parser.value("payload").toUtf8(), qos, retain); + } + }); + QObject::connect(&client, &MqttClient::subscribed, app, [](const QString &topic, Mqtt::SubscribeReturnCode subscribeReturnCode){ + if (subscribeReturnCode == Mqtt::SubscribeReturnCodeFailure) { + qWarning() << "Subscribing to topic" << topic << "failed."; + } else { + qInfo() << "Subscribed to topic filter" << topic << "with QoS" << subscribeReturnCode; + } + }); + QObject::connect(&client, &MqttClient::published, app, [](quint16 /*packetId*/, const QString &topic){ + qInfo() << "Published to topic" << topic; + }); + QObject::connect(&client, &MqttClient::publishReceived, app, [](const QString &topic, const QByteArray &payload, bool retained){ + qInfo().nospace() << "Publish received on topic " << topic << ": " << payload << (retained ? " (retained message)" : ""); + }); + + QString server = parser.positionalArguments().at(0); + if (!server.startsWith("ws://") && !server.startsWith("wss://")) { + server.prepend("mqtt://"); + } + + QUrl serverUrl = QUrl(server); + if (!serverUrl.isValid() || !QStringList({"mqtt", "ws", "wss"}).contains(serverUrl.scheme())) { + qCritical() << "Invalid server argument. Examples:"; + qCritical() << "192.168.0.1:1883"; + qCritical() << "example.com:1883"; + qCritical() << "ws://192.168.0.1:80"; + qCritical() << "wss://example.com:443"; + exit(EXIT_FAILURE); + } + + if (parser.isSet("username")) { + client.setUsername(parser.value("username")); + } + if (parser.isSet("password")) { + client.setPassword(parser.value("password")); + } + + if (serverUrl.scheme() == "mqtt") { + qDebug() << "Connecting to server" << serverUrl.host() << serverUrl.port(1883); + client.connectToHost(serverUrl.host(), serverUrl.port(1883), true, parser.isSet("ssl")); + } else { + qDebug() << "Connecting to web socket server" << serverUrl.toString(); + QNetworkRequest request(serverUrl); + client.connectToHost(request); + } + + return app->exec(); +} diff --git a/debian/control b/debian/control index ad7b4dc..fc354e5 100644 --- a/debian/control +++ b/debian/control @@ -1,8 +1,10 @@ Source: nymea-mqtt Section: comm Priority: optional -Maintainer: Michael Zanetti +Maintainer: nymea GmbH Build-Depends: debhelper, + libssl-dev, + libqt5websockets5-dev, qtbase5-dev, Standards-Version: 4.0.0 Homepage: http://nymea.io @@ -13,8 +15,8 @@ Multi-Arch: same Depends: ${misc:Depends}, ${shlibs:Depends}, -Description: nymea-mqtt libraries - nymea-mqtt is a mqtt broker implementation +Description: nymea-mqtt library + nymeas mqtt implementation for mqtt client and server development. Package: libnymea-mqtt-dev Section: devel @@ -24,8 +26,8 @@ Depends: libnymea-mqtt (=${binary:Version}), ${misc:Depends}, Description: nymea-mqtt libaries - development files - nymea-mqtt is a mqtt broker implementation. This package contains - related development files. + nymeas mqtt implementation for mqtt client and server development. + This package contains related development files. Package: nymea-mqtt-server Architecture: any @@ -33,5 +35,12 @@ Depends: ${misc:Depends}, ${shlibs:Depends}, Description: nymea-mqtt standalone server - nymea-mqtt is a mqtt broker implementation. This package contains - a standalone mqtt server. + nymeas mqtt implementation. This package contains a standalone mqtt server. + +Package: nymea-mqtt-client +Architecture: any +Depends: + ${misc:Depends}, + ${shlibs:Depends}, +Description: nymea-mqtt command line client + nymeas mqtt implementation. This package contains a command line mqtt client. diff --git a/debian/nymea-mqtt-client.install b/debian/nymea-mqtt-client.install new file mode 100644 index 0000000..874ccea --- /dev/null +++ b/debian/nymea-mqtt-client.install @@ -0,0 +1 @@ +/usr/bin/nymea-mqtt-client diff --git a/debian/nymea-mqtt-server.install b/debian/nymea-mqtt-server.install index ad0fbfe..6a34ff1 100644 --- a/debian/nymea-mqtt-server.install +++ b/debian/nymea-mqtt-server.install @@ -1 +1 @@ -/usr/bin/nymea-mqttserver +/usr/bin/nymea-mqtt-server diff --git a/libnymea-mqtt/libnymea-mqtt.pri b/libnymea-mqtt/libnymea-mqtt.pri index 568d603..ff262b8 100644 --- a/libnymea-mqtt/libnymea-mqtt.pri +++ b/libnymea-mqtt/libnymea-mqtt.pri @@ -1,28 +1,47 @@ # Include this file in your project if you want to # statically link to libnymea-mqtt +TEMPLATE = lib +TARGET = nymea-mqtt QT -= gui -QT += network +QT += network websockets CONFIG += c++11 console static -CONFIG -= app_bundle SOURCES += \ - mqttserver.cpp \ mqttpacket.cpp \ mqttsubscription.cpp \ - mqttclient.cpp + mqttserver.cpp \ + mqttclient.cpp \ + transports/mqttservertransport.cpp \ + transports/mqtttcpservertransport.cpp \ + transports/mqttwebsocketservertransport.cpp \ + transports/mqttclienttransport.cpp \ + transports/mqtttcpclienttransport.cpp \ + transports/mqttwebsocketclienttransport.cpp \ PRIVATE_HEADERS = \ mqttpacket_p.h \ mqttclient_p.h \ - mqttserver_p.h + mqttserver_p.h \ + transports/mqttservertransport.h \ + transports/mqtttcpservertransport.h \ + transports/mqttwebsocketservertransport.h \ + transports/mqttclienttransport.h \ + transports/mqtttcpclienttransport.h \ + transports/mqttwebsocketclienttransport.h \ PUBLIC_HEADERS = \ - mqttserver.h \ - mqttpacket.h \ mqtt.h \ + mqttpacket.h \ mqttsubscription.h \ + mqttserver.h \ mqttclient.h \ HEADERS += $$PRIVATE_HEADERS $$PUBLIC_HEADERS + +# https://bugreports.qt.io/browse/QTBUG-83165 +android: { + DESTDIR = $${ANDROID_TARGET_ARCH} + OBJECTS_DIR = $${ANDROID_TARGET_ARCH} +} diff --git a/libnymea-mqtt/mqtt.h b/libnymea-mqtt/mqtt.h index af1767b..f98e8ef 100644 --- a/libnymea-mqtt/mqtt.h +++ b/libnymea-mqtt/mqtt.h @@ -70,6 +70,7 @@ enum ConnectReturnCode { ConnectReturnCodeBadUsernameOrPassword = 0x04, ConnectReturnCodeNotAuthorized = 0x05 }; + enum SubscribeReturnCode { SubscribeReturnCodeSuccessQoS0 = 0x00, SubscribeReturnCodeSuccessQoS1 = 0x01, diff --git a/libnymea-mqtt/mqttclient.cpp b/libnymea-mqtt/mqttclient.cpp index d19dda4..10c7f37 100644 --- a/libnymea-mqtt/mqttclient.cpp +++ b/libnymea-mqtt/mqttclient.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -31,19 +31,24 @@ \inmodule nymea-mqtt \ingroup mqtt - MqttClient is used to connect to MQTT servers/brokers. The currently supported - MQTT protocol version is 3.1.1 including SSL encryption support. + MqttClient is used to connect to MQTT servers/brokers. + The currently supported MQTT protocol version is 3.1.1. + The currently supported transports are TCP socket and WebSocket with SSL encryption. */ #include "mqttclient.h" #include "mqttclient_p.h" #include "mqttpacket.h" +#include "transports/mqtttcpclienttransport.h" +#include "transports/mqttwebsocketclienttransport.h" + Q_LOGGING_CATEGORY(dbgClient, "nymea.mqtt.client") MqttClientPrivate::MqttClientPrivate(MqttClient *q): QObject(q), q_ptr(q) { + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); reconnectTimer.setSingleShot(true); @@ -53,49 +58,54 @@ MqttClientPrivate::MqttClientPrivate(MqttClient *q): void MqttClientPrivate::connectToHost(const QString &hostName, quint16 port, bool cleanSession, bool useSsl, const QSslConfiguration &sslConfiguration) { - if (serverHostname != hostName || serverPort != port || this->useSsl != useSsl || sslConfiguration != this->sslConfiguration) { - serverHostname = hostName; - serverPort = port; - this->useSsl = useSsl; - this->sslConfiguration = sslConfiguration; + MqttTcpClientTransport *tcpTransport = new MqttTcpClientTransport(hostName, port, useSsl, sslConfiguration, this); + connectToHost(tcpTransport, cleanSession); +} + +void MqttClientPrivate::connectToHost(const QNetworkRequest &request, bool cleanSession) +{ + MqttWebSocketClientTransport *webSocketTransport = new MqttWebSocketClientTransport(request, this); + connectToHost(webSocketTransport, cleanSession); +} + +void MqttClientPrivate::connectToHost(MqttClientTransport *transport, bool cleanSession) +{ + if (this->transport != transport) { reconnectAttempt = 1; reconnectTimer.stop(); + + if (this->transport) { + this->transport->abort(); + this->transport->deleteLater(); + } + + this->transport = transport; + + connect(transport, &MqttClientTransport::connected, this, &MqttClientPrivate::onConnected); + connect(transport, &MqttClientTransport::disconnected, this, &MqttClientPrivate::onDisconnected); + connect(transport, &MqttClientTransport::dataReceived, this, &MqttClientPrivate::onDataReceived); + connect(transport, &MqttClientTransport::stateChanged, this, &MqttClientPrivate::onSocketStateChanged); + connect(transport, &MqttClientTransport::errorSignal, this, &MqttClientPrivate::onSocketError); + connect(transport, &MqttClientTransport::sslErrors, this, &MqttClientPrivate::onSslErrors); } + this->cleanSession = cleanSession; sessionActive = true; - if (socket) { - socket->abort(); - socket->deleteLater(); - } - socket = new QSslSocket(this); - socket->setSslConfiguration(sslConfiguration); - connect(socket, &QTcpSocket::connected, this, &MqttClientPrivate::onConnected); - connect(socket, &QTcpSocket::disconnected, this, &MqttClientPrivate::onDisconnected); - connect(socket, &QTcpSocket::readyRead, this, &MqttClientPrivate::onReadyRead); - connect(socket, &QTcpSocket::stateChanged, this, &MqttClientPrivate::onSocketStateChanged); - typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); - connect(socket, static_cast(&QSslSocket::sslErrors), this, &MqttClientPrivate::onSslErrors); - typedef void (QSslSocket:: *errorSignal)(QAbstractSocket::SocketError); - connect(socket, static_cast(&QSslSocket::error), this, &MqttClientPrivate::onSocketError); - if (useSsl) { - socket->connectToHostEncrypted(hostName, port); - } else { - socket->connectToHost(hostName, port); - } + transport->connectToHost(); } void MqttClientPrivate::disconnectFromHost() { sessionActive = false; - if (!socket || !socket->isOpen()) { + if (!transport || !transport->isOpen()) { return; } MqttPacket packet(MqttPacket::TypeDisconnect); - socket->write(packet.serialize()); - socket->flush(); - socket->disconnectFromHost(); + transport->write(packet.serialize()); + transport->flush(); + transport->disconnectFromHost(); } /*! @@ -229,6 +239,11 @@ void MqttClient::connectToHost(const QString &hostName, quint16 port, bool clean d_ptr->connectToHost(hostName, port, cleanSession, useSsl, sslConfiguration); } +void MqttClient::connectToHost(const QNetworkRequest &request, bool cleanSession) +{ + d_ptr->connectToHost(request, cleanSession); +} + void MqttClient::disconnectFromHost() { d_ptr->disconnectFromHost(); @@ -236,7 +251,12 @@ void MqttClient::disconnectFromHost() bool MqttClient::isConnected() const { - return d_ptr->socket && d_ptr->socket->state() == QAbstractSocket::ConnectedState && d_ptr->keepAliveTimer.isActive(); + return d_ptr->transport && d_ptr->transport->state() == QAbstractSocket::ConnectedState && d_ptr->keepAliveTimer.isActive(); +} + +void MqttClient::ignoreSslErrors() +{ + d_ptr->transport->ignoreSslErrors(); } quint16 MqttClient::subscribe(const MqttSubscription &subscription) @@ -257,7 +277,7 @@ quint16 MqttClient::subscribe(const MqttSubscriptions &subscriptions) packet.setSubscriptions(subscriptions); d_ptr->unackedPackets.insert(packet.packetId(), packet); d_ptr->unackedPacketList.append(packet.packetId()); - d_ptr->socket->write(packet.serialize()); + d_ptr->transport->write(packet.serialize()); return packet.packetId(); } @@ -278,7 +298,7 @@ quint16 MqttClient::unsubscribe(const MqttSubscriptions &subscriptions) packet.setSubscriptions(subscriptions); d_ptr->unackedPackets.insert(packet.packetId(), packet); d_ptr->unackedPacketList.append(packet.packetId()); - d_ptr->socket->write(packet.serialize()); + d_ptr->transport->write(packet.serialize()); return packet.packetId(); } @@ -288,7 +308,7 @@ quint16 MqttClient::publish(const QString &topic, const QByteArray &payload, Mqt MqttPacket packet(MqttPacket::TypePublish, packetId, qos, retain, false); packet.setTopic(topic.toUtf8()); packet.setPayload(payload); - d_ptr->socket->write(packet.serialize()); + d_ptr->transport->write(packet.serialize()); if (qos == Mqtt::QoS0) { QTimer::singleShot(0, this, [this, packet](){ emit published(packet.packetId(), packet.topic()); @@ -313,7 +333,7 @@ void MqttClientPrivate::onConnected() packet.setWillRetain(willRetain); packet.setUsername(username.toUtf8()); packet.setPassword(password.toUtf8()); - socket->write(packet.serialize()); + transport->write(packet.serialize()); } void MqttClientPrivate::onDisconnected() @@ -322,30 +342,29 @@ void MqttClientPrivate::onDisconnected() emit q_ptr->disconnected(); if (sessionActive && autoReconnect) { reconnectAttempt = qMin(maxReconnectTimeout / 60 / 60, reconnectAttempt * 2); - qCDebug(dbgClient) << "Reconnecing in" << reconnectAttempt << "seconds"; + qCDebug(dbgClient) << "Reconnecting in" << reconnectAttempt << "seconds"; reconnectTimer.setInterval(reconnectAttempt * 1000); reconnectTimer.start(); } } -void MqttClientPrivate::onReadyRead() +void MqttClientPrivate::onDataReceived(const QByteArray &data) { - static QByteArray data; - data.append(socket->readAll()); + inputBuffer.append(data); // qCDebug(dbgClient) << "Received data from server:" << data.toHex() << "\n" << data; MqttPacket packet; - int ret = packet.parse(data); + int ret = packet.parse(inputBuffer); if (ret == -1) { qCDebug(dbgClient) << "Bad data from server. Dropping connection."; - data.clear(); - socket->abort(); + inputBuffer.clear(); + transport->abort(); return; } if (ret == 0) { qCDebug(dbgClient) << "Not enough data from server..."; return; } - data.remove(0, ret); + inputBuffer.remove(0, ret); switch (packet.type()) { case MqttPacket::TypeConnack: @@ -353,7 +372,7 @@ void MqttClientPrivate::onReadyRead() qCWarning(dbgClient) << "MQTT connection refused:" << packet.connectReturnCode(); // Always emit connected, even if just to indicate a "ClientRefusedError" emit q_ptr->connected(packet.connectReturnCode(), packet.connackFlags()); - socket->abort(); + transport->abort(); emit q_ptr->error(QAbstractSocket::ConnectionRefusedError); return; } @@ -362,7 +381,7 @@ void MqttClientPrivate::onReadyRead() if (retryPacket.type() == MqttPacket::TypePublish) { retryPacket.setDup(true); } - socket->write(retryPacket.serialize()); + transport->write(retryPacket.serialize()); } restartKeepAliveTimer(); // Make sure we emit connected after having handled all the retransmission queue @@ -377,13 +396,13 @@ void MqttClientPrivate::onReadyRead() case Mqtt::QoS1: { emit q_ptr->publishReceived(packet.topic(), packet.payload(), packet.retain()); MqttPacket response(MqttPacket::TypePuback, packet.packetId()); - socket->write(response.serialize()); + transport->write(response.serialize()); break; } case Mqtt::QoS2: { if (!packet.dup() && unackedPacketList.contains(packet.packetId())) { // Hmm... Server says it's not a duplicate, but packet id is not released yet... Drop connection. - socket->disconnectFromHost(); + transport->disconnectFromHost(); return; } @@ -394,7 +413,7 @@ void MqttClientPrivate::onReadyRead() unackedPacketList.append(packet.packetId()); emit q_ptr->publishReceived(packet.topic(), packet.payload(), packet.retain()); } - socket->write(response.serialize()); + transport->write(response.serialize()); break; } } @@ -410,7 +429,7 @@ void MqttClientPrivate::onReadyRead() MqttPacket publishPacket = unackedPackets.value(packet.packetId()); MqttPacket response(MqttPacket::TypePubrel, packet.packetId()); unackedPackets[packet.packetId()] = response; - socket->write(response.serialize()); + transport->write(response.serialize()); emit q_ptr->published(packet.packetId(), publishPacket.topic()); restartKeepAliveTimer(); break; @@ -418,7 +437,7 @@ void MqttClientPrivate::onReadyRead() case MqttPacket::TypePubrel: { MqttPacket response(MqttPacket::TypePubcomp, packet.packetId()); unackedPackets[packet.packetId()] = response; - socket->write(response.serialize()); + transport->write(response.serialize()); restartKeepAliveTimer(); break; } @@ -433,7 +452,7 @@ void MqttClientPrivate::onReadyRead() if (subscribePacket.subscriptions().count() != packet.subscribeReturnCodes().count()) { qCWarning(dbgClient) << "Subscription return code count not matching subscribe packet!"; - socket->abort(); + transport->abort(); return; } @@ -450,7 +469,7 @@ void MqttClientPrivate::onReadyRead() case MqttPacket::TypeUnsuback: if (!unackedPackets.contains(packet.packetId())) { qCWarning(dbgClient) << "UNSUBACK received but not waiting for it. Dropping connection. Packet ID:" << packet.packetId(); - socket->abort(); + transport->abort(); return; } unackedPackets.remove(packet.packetId()); @@ -465,8 +484,8 @@ void MqttClientPrivate::onReadyRead() Q_ASSERT(false); } - if (!data.isEmpty()) { - onReadyRead(); + if (!inputBuffer.isEmpty()) { + onDataReceived(QByteArray()); } } @@ -484,6 +503,7 @@ void MqttClientPrivate::onSocketError(QAbstractSocket::SocketError error) void MqttClientPrivate::onSslErrors(const QList &errors) { qCWarning(dbgClient) << "SSL error in MQTT connection:" << errors; + emit q_ptr->sslErrors(errors); } quint16 MqttClientPrivate::newPacketId() @@ -498,7 +518,7 @@ quint16 MqttClientPrivate::newPacketId() void MqttClientPrivate::sendPingreq() { MqttPacket packet(MqttPacket::TypePingreq); - socket->write(packet.serialize()); + transport->write(packet.serialize()); } void MqttClientPrivate::restartKeepAliveTimer() @@ -510,8 +530,9 @@ void MqttClientPrivate::restartKeepAliveTimer() void MqttClientPrivate::reconnectTimerTimeout() { + qCDebug(dbgClient()) << "Reconnecting now..."; if (!autoReconnect) { return; } - connectToHost(serverHostname, serverPort, false, useSsl, sslConfiguration); + connectToHost(transport, false); } diff --git a/libnymea-mqtt/mqttclient.h b/libnymea-mqtt/mqttclient.h index 4e83c44..37e00bb 100644 --- a/libnymea-mqtt/mqttclient.h +++ b/libnymea-mqtt/mqttclient.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -31,6 +31,7 @@ #include #include #include +#include #include "mqttpacket.h" #include "mqttsubscription.h" @@ -72,10 +73,13 @@ public: void setPassword(const QString &password); void connectToHost(const QString &hostName, quint16 port, bool cleanSession = true, bool useSsl = false, const QSslConfiguration &sslConfiguration = QSslConfiguration()); + void connectToHost(const QNetworkRequest &request, bool cleanSession = true); void disconnectFromHost(); bool isConnected() const; + void ignoreSslErrors(); + public slots: quint16 subscribe(const MqttSubscription &subscription); quint16 subscribe(const QString &topicFilter, Mqtt::QoS qos = Mqtt::QoS0); @@ -92,6 +96,7 @@ signals: void disconnected(); void stateChanged(QAbstractSocket::SocketState state); void error(QAbstractSocket::SocketError socketError); + void sslErrors(const QList &sslErrors); void subscribeResult(quint16 packetId, const Mqtt::SubscribeReturnCodes &subscribeReturnCodes); void subscribed(const QString &topic, Mqtt::SubscribeReturnCode subscribeReturnCode); @@ -101,7 +106,7 @@ signals: private: MqttClientPrivate *d_ptr; - friend class OperationTests; + friend class MqttTests; }; #endif // MQTTCLIENT_H diff --git a/libnymea-mqtt/mqttclient_p.h b/libnymea-mqtt/mqttclient_p.h index bb30fe9..814d266 100644 --- a/libnymea-mqtt/mqttclient_p.h +++ b/libnymea-mqtt/mqttclient_p.h @@ -30,12 +30,14 @@ #include #include +#include #include #include #include "mqttpacket.h" -#include "mqttclient.h" #include "mqttsubscription.h" +#include "mqttclient.h" +#include "transports/mqttclienttransport.h" Q_DECLARE_LOGGING_CATEGORY(dbgClient) @@ -47,12 +49,14 @@ public: MqttClient *q_ptr; void connectToHost(const QString &hostName, quint16 port, bool cleanSession, bool useSsl, const QSslConfiguration &sslConfiguration); + void connectToHost(const QNetworkRequest &request, bool cleanSession); + void connectToHost(MqttClientTransport *transport, bool cleanSession = true); void disconnectFromHost(); public slots: void onConnected(); void onDisconnected(); - void onReadyRead(); + void onDataReceived(const QByteArray &data); void onSocketStateChanged(QAbstractSocket::SocketState socketState); void onSocketError(QAbstractSocket::SocketError error); void onSslErrors(const QList &errors); @@ -64,14 +68,10 @@ public slots: void reconnectTimerTimeout(); public: - QString serverHostname; - quint16 serverPort = 0; - bool useSsl = false; - QSslConfiguration sslConfiguration; bool autoReconnect = true; bool sessionActive = false; bool cleanSession = true; - QSslSocket *socket = nullptr; + MqttClientTransport *transport = nullptr; QTimer reconnectTimer; int reconnectAttempt = 0; quint16 maxReconnectTimeout = 36000; @@ -88,6 +88,8 @@ public: QVector unackedPacketList; QHash unackedPackets; + + QByteArray inputBuffer; }; #endif // MQTTCLIENT_P_H diff --git a/libnymea-mqtt/mqttserver.cpp b/libnymea-mqtt/mqttserver.cpp index f9fe1eb..55b2ab1 100644 --- a/libnymea-mqtt/mqttserver.cpp +++ b/libnymea-mqtt/mqttserver.cpp @@ -61,6 +61,8 @@ #include "mqttserver.h" #include "mqttserver_p.h" +#include "transports/mqtttcpservertransport.h" +#include "transports/mqttwebsocketservertransport.h" #include "mqttpacket.h" #include @@ -78,10 +80,25 @@ MqttServerPrivate::MqttServerPrivate(MqttServer *q): qRegisterMetaType(); } +int MqttServerPrivate::listen(MqttServerTransport *transport, const QHostAddress &address, quint16 port) +{ + connect(transport, &MqttServerTransport::clientConnected, this, &MqttServerPrivate::onClientConnected); + + if (!transport->listen(address, port)) { + qCWarning(dbgServer) << "Error listening on port" << port; + transport->deleteLater(); + return -1; + } + static int addressId = -1; + servers.insert(++addressId, transport); + qCDebug(dbgServer) << "nymea MQTT server running on" << address.toString() << ":" << port << "( Address ID" << addressId << ")"; + return addressId; +} + QHash MqttServerPrivate::publish(const QString &topic, const QByteArray &payload) { - QHash receivers; - foreach (QTcpSocket *c, clientList.keys()) { + QHash receivers; + foreach (MqttServerClient *c, clientList.keys()) { foreach (const MqttSubscription &subscription, clientList.value(c)->subscriptions) { if (matchTopic(subscription.topicFilter(), topic)) { if (!receivers.contains(c) || receivers.value(c) < subscription.qoS()) { @@ -92,7 +109,7 @@ QHash MqttServerPrivate::publish(const QString &topic, const Q } QHash packets; - foreach (QTcpSocket *receiver, receivers.keys()) { + foreach (MqttServerClient *receiver, receivers.keys()) { ClientContext *ctx = clientList.value(receiver); qCDebug(dbgServer) << "Relaying packet to subscribed client:" << ctx->clientId; Mqtt::QoS qos = receivers.value(receiver); @@ -139,26 +156,22 @@ void MqttServer::setAuthorizer(MqttAuthorizer *authorizer) int MqttServer::listen(const QHostAddress &address, quint16 port, const QSslConfiguration &sslConfiguration) { - SslServer *server = new SslServer(sslConfiguration, this); - connect(server, &SslServer::clientConnected, d_ptr, &MqttServerPrivate::onClientConnected); - connect(server, &SslServer::clientDisconnected, d_ptr, &MqttServerPrivate::onClientDisconnected); - connect(server, &SslServer::dataAvailable, d_ptr, &MqttServerPrivate::onDataAvailable); + qCDebug(dbgServer) << "Starting nymea MQTT server on TCP"; + MqttServerTransport *transport = new MqttTcpServerTransport(sslConfiguration, this); + return d_ptr->listen(transport, address, port); +} - if (!server->listen(address, port)) { - qCWarning(dbgServer) << "Error listening on port" << port; - server->deleteLater(); - return -1; - } - static int addressId = -1; - d_ptr->servers.insert(++addressId, server); - qCDebug(dbgServer) << "nymea MQTT server running on" << address.toString() << ":" << port << "( Address ID" << addressId << ")"; - return addressId; +int MqttServer::listenWebSocket(const QHostAddress &address, quint16 port, const QSslConfiguration &sslConfiguration) +{ + qCDebug(dbgServer) << "Starting nymea MQTT server on WebSocket"; + MqttServerTransport *transport = new MqttWebSocketServerTransport(sslConfiguration, this); + return d_ptr->listen(transport, address, port); } bool MqttServer::isListening(const QHostAddress &address, quint16 port) const { - foreach (SslServer *server, d_ptr->servers) { - if (server->serverAddress() == address && server->serverPort() == port && server->isListening()) { + foreach (MqttServerTransport *transport, d_ptr->servers) { + if (transport->serverAddress() == address && transport->serverPort() == port && transport->isListening()) { return true; } } @@ -176,12 +189,12 @@ void MqttServer::close(int interfaceId) qCWarning(dbgServer) << "No such server address ID" << interfaceId; return; } - SslServer *server = d_ptr->servers.take(interfaceId); - while (!d_ptr->clientServerMap.keys(server).isEmpty()) { - d_ptr->cleanupClient(d_ptr->clientServerMap.keys(server).first()); + MqttServerTransport *transport = d_ptr->servers.take(interfaceId); + while (!d_ptr->clientServerMap.keys(transport).isEmpty()) { + d_ptr->cleanupClient(d_ptr->clientServerMap.keys(transport).first()); } - server->close(); - server->deleteLater(); + transport->close(); + transport->deleteLater(); } QStringList MqttServer::clients() const @@ -208,9 +221,12 @@ QHash MqttServer::publish(const QString &topic, const QByteArr return d_ptr->publish(topic, payload); } -void MqttServerPrivate::onClientConnected(QSslSocket *client) +void MqttServerPrivate::onClientConnected(MqttServerClient *client) { - SslServer *server = static_cast(sender()); + connect(client, &MqttServerClient::dataAvailable, this, &MqttServerPrivate::onDataAvailable); + connect(client, &MqttServerClient::disconnected, this, &MqttServerPrivate::onClientDisconnected); + + MqttServerTransport *transport = static_cast(sender()); // Start a 10 second timer to clean up the connection if we don't get data until then. QTimer *timeoutTimer = new QTimer(this); @@ -221,12 +237,14 @@ void MqttServerPrivate::onClientConnected(QSslSocket *client) client->deleteLater(); }); timeoutTimer->start(10000); - clientServerMap.insert(client, server); + clientServerMap.insert(client, transport); pendingConnections.insert(client, timeoutTimer); } -void MqttServerPrivate::onDataAvailable(QSslSocket *client, const QByteArray &data) +void MqttServerPrivate::onDataAvailable(const QByteArray &data) { + MqttServerClient *client = qobject_cast(sender()); + clientBuffers[client].append(data); do { @@ -256,12 +274,13 @@ void MqttServerPrivate::onDataAvailable(QSslSocket *client, const QByteArray &da } while (!clientBuffers.value(client).isEmpty()); } -void MqttServerPrivate::onClientDisconnected(QSslSocket *client) +void MqttServerPrivate::onClientDisconnected() { + MqttServerClient *client = qobject_cast(sender()); cleanupClient(client); } -void MqttServerPrivate::cleanupClient(QTcpSocket *client) +void MqttServerPrivate::cleanupClient(MqttServerClient *client) { if (clientBuffers.contains(client)) { clientBuffers.remove(client); @@ -302,7 +321,7 @@ void MqttServerPrivate::cleanupClient(QTcpSocket *client) client->deleteLater(); } -void MqttServerPrivate::processPacket(const MqttPacket &packet, QTcpSocket *client) +void MqttServerPrivate::processPacket(const MqttPacket &packet, MqttServerClient *client) { if (packet.type() == MqttPacket::TypeConnect) { if (clientList.contains(client)) { @@ -343,8 +362,8 @@ void MqttServerPrivate::processPacket(const MqttPacket &packet, QTcpSocket *clie if (packet.connectFlags().testFlag(Mqtt::ConnectFlagPassword)) { password = packet.password(); } - SslServer *server = clientServerMap.value(client); - int serverAddressId = servers.key(server); + MqttServerTransport *transport = clientServerMap.value(client); + int serverAddressId = servers.key(transport); Mqtt::ConnectReturnCode userValidationReturnCode = authorizer->authorizeConnect(serverAddressId, clientId, username, password, client->peerAddress()); if (userValidationReturnCode != Mqtt::ConnectReturnCodeAccepted) { qCWarning(dbgServer) << "Rejecting connection due to user validation."; @@ -357,9 +376,9 @@ void MqttServerPrivate::processPacket(const MqttPacket &packet, QTcpSocket *clie ClientContext *ctx = nullptr; - QList existingSockets = clientList.keys(); - for (int i = 0; i < existingSockets.count(); i++) { - QTcpSocket *existingClient = existingSockets.at(i); + QList existingClients = clientList.keys(); + for (int i = 0; i < existingClients.count(); i++) { + MqttServerClient *existingClient = existingClients.at(i); if (clientId == clientList.value(existingClient)->clientId) { if (!packet.connectFlags().testFlag(Mqtt::ConnectFlagCleanSession)) { qCDebug(dbgServer).nospace() << clientId << ": Already have a session for this client ID. Taking over existing session."; @@ -371,6 +390,7 @@ void MqttServerPrivate::processPacket(const MqttPacket &packet, QTcpSocket *clie clientList.remove(existingClient); clientBuffers.remove(existingClient); existingClient->flush(); + existingClient->abort(); existingClient->deleteLater(); } else { qCDebug(dbgServer).nospace() << clientId << ": Already have a session for this client ID. Dropping old session."; @@ -690,40 +710,3 @@ quint16 MqttServerPrivate::newPacketId(ClientContext *ctx) return packetId; } -void SslServer::incomingConnection(qintptr socketDescriptor) -{ - QSslSocket *sslSocket = new QSslSocket(this); - - qCDebug(dbgServer) << "New client socket connection:" << sslSocket; - - connect(sslSocket, &QSslSocket::encrypted, [this, sslSocket](){ emit clientConnected(sslSocket); }); - connect(sslSocket, &QSslSocket::readyRead, this, &SslServer::onSocketReadyRead); - connect(sslSocket, &QSslSocket::disconnected, this, &SslServer::onClientDisconnected); - - if (!sslSocket->setSocketDescriptor(socketDescriptor)) { - qCWarning(dbgServer) << "Failed to set SSL socket descriptor."; - delete sslSocket; - return; - } - if (!m_config.isNull()) { - sslSocket->setSslConfiguration(m_config); - sslSocket->startServerEncryption(); - } else { - emit clientConnected(sslSocket); - } -} - -void SslServer::onClientDisconnected() -{ - QSslSocket *socket = static_cast(sender()); - qCDebug(dbgServer) << "Client socket disconnected:" << socket; - emit clientDisconnected(socket); - socket->deleteLater(); -} - -void SslServer::onSocketReadyRead() -{ - QSslSocket *socket = static_cast(sender()); - QByteArray data = socket->readAll(); - emit dataAvailable(socket, data); -} diff --git a/libnymea-mqtt/mqttserver.h b/libnymea-mqtt/mqttserver.h index 7ba9fcb..d8c9883 100644 --- a/libnymea-mqtt/mqttserver.h +++ b/libnymea-mqtt/mqttserver.h @@ -29,9 +29,8 @@ #define MQTTSERVER_H #include -#include -#include #include +#include #include #include @@ -60,6 +59,7 @@ public: void setAuthorizer(MqttAuthorizer *authorizer); int listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 1883, const QSslConfiguration &sslConfiguration = QSslConfiguration()); + int listenWebSocket(const QHostAddress &address = QHostAddress::Any, quint16 port = 80, const QSslConfiguration &sslConfiguration = QSslConfiguration()); QList listeningAddressIds() const; QPair listeningAddress(int addressId); void close(int addressId); diff --git a/libnymea-mqtt/mqttserver_p.h b/libnymea-mqtt/mqttserver_p.h index 1c4dadc..0d8d9b8 100644 --- a/libnymea-mqtt/mqttserver_p.h +++ b/libnymea-mqtt/mqttserver_p.h @@ -41,7 +41,8 @@ Q_DECLARE_LOGGING_CATEGORY(dbgServer) class ClientContext; class Subscription; -class SslServer; +class MqttServerTransport; +class MqttServerClient; class MqttServerPrivate: public QObject { @@ -49,34 +50,33 @@ class MqttServerPrivate: public QObject public: explicit MqttServerPrivate(MqttServer *q); + int listen(MqttServerTransport *transport, const QHostAddress &address, quint16 port); QHash publish(const QString &topic, const QByteArray &payload = QByteArray()); + void cleanupClient(MqttServerClient *client); -public: - void cleanupClient(QTcpSocket *client); - - void processPacket(const MqttPacket &packet, QTcpSocket *client); + void processPacket(const MqttPacket &packet, MqttServerClient *client); bool validateTopicFilter(const QString &topicFilter); bool matchTopic(const QString &topicFilter, const QString &topic); quint16 newPacketId(ClientContext *ctx); public slots: - void onClientConnected(QSslSocket *client); - void onDataAvailable(QSslSocket *client, const QByteArray &data); - void onClientDisconnected(QSslSocket *client); + void onClientConnected(MqttServerClient *client); + void onDataAvailable(const QByteArray &data); + void onClientDisconnected(); public: MqttServer *q_ptr; - QHash servers; + QHash servers; MqttAuthorizer *authorizer = nullptr; Mqtt::QoS maximumSubscriptionQoS = Mqtt::QoS2; - QHash pendingConnections; - QHash clientList; - QHash clientBuffers; + QHash pendingConnections; + QHash clientList; + QHash clientBuffers; QHash retainedMessages; - QHash clientServerMap; + QHash clientServerMap; }; class ClientContext { @@ -98,31 +98,4 @@ public: QHash unackedPackets; }; -class SslServer: public QTcpServer -{ - Q_OBJECT -public: - SslServer(const QSslConfiguration &config, QObject *parent = nullptr): - QTcpServer(parent), - m_config(config) - { - - } - -signals: - void clientConnected(QSslSocket *socket); - void clientDisconnected(QSslSocket *socket); - void dataAvailable(QSslSocket *socket, const QByteArray &data); - -protected: - void incomingConnection(qintptr socketDescriptor) override; - -private slots: - void onClientDisconnected(); - void onSocketReadyRead(); - -private: - QSslConfiguration m_config; -}; - #endif // MQTTSERVER_P_H diff --git a/libnymea-mqtt/transports/mqttclienttransport.cpp b/libnymea-mqtt/transports/mqttclienttransport.cpp new file mode 100644 index 0000000..299ca50 --- /dev/null +++ b/libnymea-mqtt/transports/mqttclienttransport.cpp @@ -0,0 +1,34 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttclienttransport.h" + +MqttClientTransport::MqttClientTransport(QObject *parent) + : QObject(parent) +{ + +} diff --git a/libnymea-mqtt/transports/mqttclienttransport.h b/libnymea-mqtt/transports/mqttclienttransport.h new file mode 100644 index 0000000..b6f8d5b --- /dev/null +++ b/libnymea-mqtt/transports/mqttclienttransport.h @@ -0,0 +1,66 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTCLIENTTRANSPORT_H +#define MQTTCLIENTTRANSPORT_H + +#include +#include +#include +#include +#include + +#include +Q_DECLARE_LOGGING_CATEGORY(dbgClient) + +class MqttClientTransport : public QObject +{ + Q_OBJECT +public: + explicit MqttClientTransport(QObject *parent = nullptr); + virtual ~MqttClientTransport() = default; + + virtual void connectToHost() = 0; + virtual void abort() = 0; + virtual bool isOpen() const = 0; + virtual bool write(const QByteArray &data) = 0; + virtual void flush() = 0; + virtual void disconnectFromHost() = 0; + virtual QAbstractSocket::SocketState state() const = 0; + virtual void ignoreSslErrors() = 0; + +signals: + void connected(); + void disconnected(); + void dataReceived(const QByteArray &data); + void stateChanged(QAbstractSocket::SocketState state); + void errorSignal(QAbstractSocket::SocketError error); + void sslErrors(const QList &sslErrors); + +}; + +#endif // MQTTCLIENTTRANSPORT_H diff --git a/libnymea-mqtt/transports/mqttservertransport.cpp b/libnymea-mqtt/transports/mqttservertransport.cpp new file mode 100644 index 0000000..24d6b91 --- /dev/null +++ b/libnymea-mqtt/transports/mqttservertransport.cpp @@ -0,0 +1,47 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttservertransport.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(dbgServer) + +MqttServerClient::MqttServerClient(QObject *parent): + QObject(parent) +{ + +} + +MqttServerTransport::MqttServerTransport(QObject *parent): + QObject(parent) +{ + +} + + diff --git a/libnymea-mqtt/transports/mqttservertransport.h b/libnymea-mqtt/transports/mqttservertransport.h new file mode 100644 index 0000000..d1fb6ed --- /dev/null +++ b/libnymea-mqtt/transports/mqttservertransport.h @@ -0,0 +1,73 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTSERVERTRANSPORT_H +#define MQTTSERVERTRANSPORT_H + +#include +#include + +class QTcpServer; + +class MqttServerClient: public QObject +{ + Q_OBJECT +public: + explicit MqttServerClient(QObject *parent = nullptr); + virtual ~MqttServerClient() = default; + + virtual bool write(const QByteArray &data) = 0; + virtual void abort() = 0; + virtual bool isOpen() const = 0; + virtual void flush() = 0; + virtual void close() = 0; + virtual QHostAddress peerAddress() const = 0; + +signals: + void dataAvailable(const QByteArray &data); + void disconnected(); +}; + +class MqttServerTransport : public QObject +{ + Q_OBJECT +public: + explicit MqttServerTransport(QObject *parent = nullptr); + virtual ~MqttServerTransport() = default; + + virtual bool listen(const QHostAddress &address, int port) = 0; + virtual bool isListening() const = 0; + virtual QHostAddress serverAddress() const = 0; + virtual int serverPort() const = 0; + virtual void close() = 0; + +signals: + void clientConnected(MqttServerClient *client); +}; + + +#endif // MQTTSERVERTRANSPORT_H diff --git a/libnymea-mqtt/transports/mqtttcpclienttransport.cpp b/libnymea-mqtt/transports/mqtttcpclienttransport.cpp new file mode 100644 index 0000000..7f8d9af --- /dev/null +++ b/libnymea-mqtt/transports/mqtttcpclienttransport.cpp @@ -0,0 +1,99 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqtttcpclienttransport.h" + +MqttTcpClientTransport::MqttTcpClientTransport(const QString &hostName, quint16 port, bool useSsl, const QSslConfiguration &sslConfiguration, QObject *parent): + MqttClientTransport(parent), + m_hostName(hostName), + m_port(port), + m_useSsl(useSsl) +{ + m_socket = new QSslSocket(this); + m_socket->setSslConfiguration(sslConfiguration); + + connect(m_socket, &QTcpSocket::connected, this, &MqttClientTransport::connected); + connect(m_socket, &QTcpSocket::disconnected, this, &MqttClientTransport::disconnected); + connect(m_socket, &QTcpSocket::stateChanged, this, &MqttClientTransport::stateChanged); + typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); + connect(m_socket, static_cast(&QSslSocket::sslErrors), this, &MqttClientTransport::sslErrors); + typedef void (QSslSocket:: *errorSignal)(QAbstractSocket::SocketError); + connect(m_socket, static_cast(&QSslSocket::error), this, &MqttClientTransport::errorSignal); + + connect(m_socket, &QTcpSocket::readyRead, this, &MqttTcpClientTransport::onReadyRead); +} + +void MqttTcpClientTransport::connectToHost() +{ + if (m_useSsl) { + m_socket->connectToHostEncrypted(m_hostName, m_port); + } else { + m_socket->connectToHost(m_hostName, m_port); + } +} + +void MqttTcpClientTransport::abort() +{ + m_socket->abort(); +} + +bool MqttTcpClientTransport::isOpen() const +{ + return m_socket->isOpen(); +} + +bool MqttTcpClientTransport::write(const QByteArray &data) +{ + int ret = m_socket->write(data); + return ret == data.length(); +} + +void MqttTcpClientTransport::flush() +{ + m_socket->flush(); +} + +void MqttTcpClientTransport::disconnectFromHost() +{ + m_socket->disconnectFromHost(); +} + +QAbstractSocket::SocketState MqttTcpClientTransport::state() const +{ + return m_socket->state(); +} + +void MqttTcpClientTransport::ignoreSslErrors() +{ + m_socket->ignoreSslErrors(); +} + +void MqttTcpClientTransport::onReadyRead() +{ + QByteArray data = m_socket->readAll(); + emit dataReceived(data); +} diff --git a/libnymea-mqtt/transports/mqtttcpclienttransport.h b/libnymea-mqtt/transports/mqtttcpclienttransport.h new file mode 100644 index 0000000..595600f --- /dev/null +++ b/libnymea-mqtt/transports/mqtttcpclienttransport.h @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTTCPCLIENTTRANSPORT_H +#define MQTTTCPCLIENTTRANSPORT_H + +#include "mqttclienttransport.h" + +class MqttTcpClientTransport: public MqttClientTransport +{ + Q_OBJECT +public: + explicit MqttTcpClientTransport(const QString &hostName, quint16 port, bool useSsl, const QSslConfiguration &sslConfiguration, QObject *parent = nullptr); + + void connectToHost() override; + + void abort() override; + bool isOpen() const override; + bool write(const QByteArray &data) override; + void flush() override; + void disconnectFromHost() override; + QAbstractSocket::SocketState state() const override; + void ignoreSslErrors() override; + +private slots: + void onReadyRead(); + +private: + QString m_hostName; + quint16 m_port; + bool m_useSsl = false; + QSslSocket *m_socket = nullptr; +}; + +#endif diff --git a/libnymea-mqtt/transports/mqtttcpservertransport.cpp b/libnymea-mqtt/transports/mqtttcpservertransport.cpp new file mode 100644 index 0000000..19c4272 --- /dev/null +++ b/libnymea-mqtt/transports/mqtttcpservertransport.cpp @@ -0,0 +1,153 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqtttcpservertransport.h" + +#include + +Q_DECLARE_LOGGING_CATEGORY(dbgServer) + + +SslServer::SslServer(const QSslConfiguration &config, QObject *parent): + QTcpServer(parent), + m_config(config) +{ + +} + +void SslServer::incomingConnection(qintptr socketDescriptor) +{ + QSslSocket *sslSocket = new QSslSocket(this); + + qCDebug(dbgServer) << "New client socket connection:" << sslSocket; + + connect(sslSocket, &QSslSocket::encrypted, [this, sslSocket](){ emit clientConnected(sslSocket); }); + connect(sslSocket, &QSslSocket::disconnected, this, &SslServer::onClientDisconnected); + + if (!sslSocket->setSocketDescriptor(socketDescriptor)) { + qCWarning(dbgServer) << "Failed to set SSL socket descriptor."; + delete sslSocket; + return; + } + if (!m_config.isNull()) { + sslSocket->setSslConfiguration(m_config); + sslSocket->startServerEncryption(); + } else { + emit clientConnected(sslSocket); + } +} + +void SslServer::onClientDisconnected() +{ + QSslSocket *socket = static_cast(sender()); + qCDebug(dbgServer) << "Client socket disconnected:" << socket; + emit clientDisconnected(socket); + socket->deleteLater(); +} + +MqttTcpServerClient::MqttTcpServerClient(QTcpSocket *socket, QObject *parent): + MqttServerClient(parent), + m_socket(socket) +{ + m_socket->setParent(this); + connect(socket, &QTcpSocket::readyRead, this, &MqttTcpServerClient::onSocketReadyRead); + connect(socket, &QTcpSocket::disconnected, this, &MqttTcpServerClient::disconnected); +} + +void MqttTcpServerClient::onSocketReadyRead() +{ + emit dataAvailable(m_socket->readAll()); +} + +bool MqttTcpServerClient::write(const QByteArray &data) +{ + qint64 len = m_socket->write(data); + return len == data.length(); +} + +void MqttTcpServerClient::abort() +{ + m_socket->abort(); +} + +bool MqttTcpServerClient::isOpen() const +{ + return m_socket->isOpen(); +} + +void MqttTcpServerClient::flush() +{ + m_socket->flush(); +} + +void MqttTcpServerClient::close() +{ + m_socket->close(); +} + +QHostAddress MqttTcpServerClient::peerAddress() const +{ + return m_socket->peerAddress(); +} + +MqttTcpServerTransport::MqttTcpServerTransport(const QSslConfiguration &config, QObject *parent): + MqttServerTransport(parent), + m_sslServer(new SslServer(config, this)) +{ + connect(m_sslServer, &SslServer::clientConnected, this, &MqttTcpServerTransport::onClientConnected); +} + +bool MqttTcpServerTransport::listen(const QHostAddress &address, int port) +{ + return m_sslServer->listen(address, port); +} + +bool MqttTcpServerTransport::isListening() const +{ + return m_sslServer->isListening(); +} + +QHostAddress MqttTcpServerTransport::serverAddress() const +{ + return m_sslServer->serverAddress(); +} + +int MqttTcpServerTransport::serverPort() const +{ + return m_sslServer->serverPort(); +} + +void MqttTcpServerTransport::close() +{ + return m_sslServer->close(); +} + +void MqttTcpServerTransport::onClientConnected(QTcpSocket *socket) +{ + MqttTcpServerClient *client = new MqttTcpServerClient(socket, this); + emit clientConnected(client); +} diff --git a/libnymea-mqtt/transports/mqtttcpservertransport.h b/libnymea-mqtt/transports/mqtttcpservertransport.h new file mode 100644 index 0000000..d139ac2 --- /dev/null +++ b/libnymea-mqtt/transports/mqtttcpservertransport.h @@ -0,0 +1,96 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTTCPSERVERTRANSPORT_H +#define MQTTTCPSERVERTRANSPORT_H + +#include "mqttservertransport.h" + +#include +#include + +class SslServer: public QTcpServer +{ + Q_OBJECT +public: + SslServer(const QSslConfiguration &config, QObject *parent = nullptr); + +signals: + void clientConnected(QSslSocket *socket); + void clientDisconnected(QSslSocket *socket); + void dataAvailable(QSslSocket *socket, const QByteArray &data); + +protected: + void incomingConnection(qintptr socketDescriptor) override; + +private slots: + void onClientDisconnected(); + +private: + QSslConfiguration m_config; +}; + +class MqttTcpServerClient: public MqttServerClient +{ + Q_OBJECT +public: + explicit MqttTcpServerClient(QTcpSocket *socket, QObject *parent = nullptr); + + bool write(const QByteArray &data) override; + void abort() override; + bool isOpen() const override; + void flush() override; + void close() override; + QHostAddress peerAddress() const override; + +private slots: + void onSocketReadyRead(); + +private: + QTcpSocket *m_socket = nullptr; +}; + +class MqttTcpServerTransport: public MqttServerTransport +{ + Q_OBJECT +public: + explicit MqttTcpServerTransport(const QSslConfiguration &config, QObject *parent = nullptr); + + bool listen(const QHostAddress &address, int port) override; + bool isListening() const override; + QHostAddress serverAddress() const override; + int serverPort() const override; + void close() override; + +private slots: + void onClientConnected(QTcpSocket *socket); + +private: + SslServer *m_sslServer = nullptr; +}; + +#endif // MQTTTCPSERVERTRANSPORT_H diff --git a/libnymea-mqtt/transports/mqttwebsocketclienttransport.cpp b/libnymea-mqtt/transports/mqttwebsocketclienttransport.cpp new file mode 100644 index 0000000..1486aab --- /dev/null +++ b/libnymea-mqtt/transports/mqttwebsocketclienttransport.cpp @@ -0,0 +1,105 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttwebsocketclienttransport.h" + +MqttWebSocketClientTransport::MqttWebSocketClientTransport(const QNetworkRequest &request, QObject *parent): + MqttClientTransport(parent), + m_request(request) +{ + m_socket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this); + + connect(m_socket, &QWebSocket::connected, this, &MqttClientTransport::connected); + connect(m_socket, &QWebSocket::disconnected, this, &MqttClientTransport::disconnected); + connect(m_socket, &QWebSocket::stateChanged, this, &MqttClientTransport::stateChanged); + connect(m_socket, &QWebSocket::sslErrors, this, &MqttClientTransport::sslErrors); + typedef void (QWebSocket:: *errorSignal)(QAbstractSocket::SocketError); + connect(m_socket, static_cast(&QWebSocket::error), this, &MqttClientTransport::errorSignal); + + connect(m_socket, &QWebSocket::textMessageReceived, this, &MqttWebSocketClientTransport::onTextMessageReceived); + connect(m_socket, &QWebSocket::binaryMessageReceived, this, &MqttWebSocketClientTransport::onBinaryMessageReceived); +} + +void MqttWebSocketClientTransport::connectToHost() +{ +#if QT_VERSION < QT_VERSION_CHECK(5, 6, 0) + if (!m_request.rawHeaderList().isEmpty()) { + qCWarning(dbgClient) << "Qt versions older than 5.6 do not support HTTP request headers with web sockets. The connection may fail."; + } + m_socket->open(m_request.url()); +#else + m_socket->open(m_request); +#endif +} + +void MqttWebSocketClientTransport::abort() +{ + m_socket->abort(); +} + +bool MqttWebSocketClientTransport::isOpen() const +{ + return m_socket->isValid(); +} + +bool MqttWebSocketClientTransport::write(const QByteArray &data) +{ + int ret = m_socket->sendBinaryMessage(data); + return ret == data.length(); +} + +void MqttWebSocketClientTransport::flush() +{ + m_socket->flush(); +} + +void MqttWebSocketClientTransport::disconnectFromHost() +{ + m_socket->close(); +} + +QAbstractSocket::SocketState MqttWebSocketClientTransport::state() const +{ + return m_socket->state(); +} + +void MqttWebSocketClientTransport::ignoreSslErrors() +{ + m_socket->ignoreSslErrors(); +} + +void MqttWebSocketClientTransport::onTextMessageReceived(const QString &message) +{ + qCDebug(dbgClient()) << "Text message received:" << message; + qCWarning(dbgClient()) << "Received a text message from the server. Doesn't look like MQTT. Closing connection."; + m_socket->close(QWebSocketProtocol::CloseCodeProtocolError); +} + +void MqttWebSocketClientTransport::onBinaryMessageReceived(const QByteArray &message) +{ + emit dataReceived(message); +} diff --git a/libnymea-mqtt/transports/mqttwebsocketclienttransport.h b/libnymea-mqtt/transports/mqttwebsocketclienttransport.h new file mode 100644 index 0000000..0213716 --- /dev/null +++ b/libnymea-mqtt/transports/mqttwebsocketclienttransport.h @@ -0,0 +1,58 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTWEBSOCKETCLIENTTRANSPORT_H +#define MQTTWEBSOCKETCLIENTTRANSPORT_H + +#include "mqttclienttransport.h" + +class MqttWebSocketClientTransport: public MqttClientTransport +{ + Q_OBJECT +public: + explicit MqttWebSocketClientTransport(const QNetworkRequest &request, QObject *parent = nullptr); + + void connectToHost() override; + + void abort() override; + bool isOpen() const override; + bool write(const QByteArray &data) override; + void flush() override; + void disconnectFromHost() override; + QAbstractSocket::SocketState state() const override; + void ignoreSslErrors() override; + +private slots: + void onTextMessageReceived(const QString &message); + void onBinaryMessageReceived(const QByteArray &message); + +private: + QNetworkRequest m_request; + QWebSocket *m_socket = nullptr; +}; + +#endif diff --git a/libnymea-mqtt/transports/mqttwebsocketservertransport.cpp b/libnymea-mqtt/transports/mqttwebsocketservertransport.cpp new file mode 100644 index 0000000..ff3bd94 --- /dev/null +++ b/libnymea-mqtt/transports/mqttwebsocketservertransport.cpp @@ -0,0 +1,134 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttwebsocketservertransport.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(dbgServer) + +MqttWebSocketServerClient::MqttWebSocketServerClient(QWebSocket *socket, QObject *parent): + MqttServerClient(parent), + m_socket(socket) +{ + m_socket->setParent(this); + connect(m_socket, &QWebSocket::textMessageReceived, this, &MqttWebSocketServerClient::onTextMessageReceived); + connect(m_socket, &QWebSocket::binaryMessageReceived, this, &MqttWebSocketServerClient::onBinaryMessageReceived); + connect(m_socket, &QWebSocket::disconnected, this, &MqttServerClient::disconnected); +} + +bool MqttWebSocketServerClient::write(const QByteArray &data) +{ + qint64 len = m_socket->sendBinaryMessage(data); + return len == data.length(); +} + +void MqttWebSocketServerClient::abort() +{ + m_socket->abort(); +} + +bool MqttWebSocketServerClient::isOpen() const +{ + return m_socket->isValid(); +} + +void MqttWebSocketServerClient::flush() +{ + m_socket->flush(); +} + +void MqttWebSocketServerClient::close() +{ + m_socket->close(); +} + +QHostAddress MqttWebSocketServerClient::peerAddress() const +{ + return m_socket->peerAddress(); +} + +void MqttWebSocketServerClient::onTextMessageReceived(const QString &message) +{ + qCWarning(dbgServer).nospace() << "WebSocket received a text message from " << peerAddress() << ": " << message << ". This is not valid. Closing connection."; + m_socket->abort(); +} + +void MqttWebSocketServerClient::onBinaryMessageReceived(const QByteArray &data) +{ + emit dataAvailable(data); +} + +MqttWebSocketServerTransport::MqttWebSocketServerTransport(const QSslConfiguration &sslConfiguration, QObject *parent): + MqttServerTransport(parent) +{ + if (sslConfiguration.isNull()) { + m_server = new QWebSocketServer("nymea-mqtt", QWebSocketServer::NonSecureMode, this); + } else { + m_server = new QWebSocketServer("nymea-mqtt", QWebSocketServer::SecureMode, this); + m_server->setSslConfiguration(sslConfiguration); + } + connect(m_server, &QWebSocketServer::newConnection, this, &MqttWebSocketServerTransport::onNewConnection); +} + +bool MqttWebSocketServerTransport::listen(const QHostAddress &address, int port) +{ + return m_server->listen(address, port); +} + +bool MqttWebSocketServerTransport::isListening() const +{ + return m_server->isListening(); +} + +QHostAddress MqttWebSocketServerTransport::serverAddress() const +{ + return m_server->serverAddress(); +} + +int MqttWebSocketServerTransport::serverPort() const +{ + return m_server->serverPort(); +} + +void MqttWebSocketServerTransport::close() +{ + m_server->close(); +} + +void MqttWebSocketServerTransport::onNewConnection() +{ + QWebSocket *webSocket = m_server->nextPendingConnection(); + if (!webSocket) { + qCWarning(dbgServer()) << "New connection signalled but no pending socket available"; + return; + } + MqttWebSocketServerClient *client = new MqttWebSocketServerClient(webSocket, this); + emit clientConnected(client); +} + diff --git a/libnymea-mqtt/transports/mqttwebsocketservertransport.h b/libnymea-mqtt/transports/mqttwebsocketservertransport.h new file mode 100644 index 0000000..2b9da4c --- /dev/null +++ b/libnymea-mqtt/transports/mqttwebsocketservertransport.h @@ -0,0 +1,79 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTWEBSOCKETSERVERTRANSPORT_H +#define MQTTWEBSOCKETSERVERTRANSPORT_H + +#include "mqttservertransport.h" + +#include + +class MqttWebSocketServerClient: public MqttServerClient +{ + Q_OBJECT +public: + explicit MqttWebSocketServerClient(QWebSocket *socket, QObject *parent = nullptr); + + bool write(const QByteArray &data) override; + void abort() override; + bool isOpen() const override; + void flush() override; + void close() override; + QHostAddress peerAddress() const override; + +private slots: + void onTextMessageReceived(const QString &message); + void onBinaryMessageReceived(const QByteArray &data); + +private: + QWebSocket *m_socket = nullptr; + +}; + +class MqttWebSocketServerTransport : public MqttServerTransport +{ + Q_OBJECT +public: + explicit MqttWebSocketServerTransport(const QSslConfiguration &sslConfiguration, QObject *parent = nullptr); + + bool listen(const QHostAddress &address, int port) override; + bool isListening() const override; + QHostAddress serverAddress() const override; + int serverPort() const override; + void close() override; + +signals: + +private slots: + void onNewConnection(); + +private: + QWebSocketServer *m_server = nullptr; + +}; + +#endif // MQTTWEBSOCKETSERVERTRANSPORT_H diff --git a/nymea-mqtt.pro b/nymea-mqtt.pro index 3d38718..01c1be8 100644 --- a/nymea-mqtt.pro +++ b/nymea-mqtt.pro @@ -1,6 +1,7 @@ TEMPLATE = subdirs -SUBDIRS += libnymea-mqtt server tests +SUBDIRS += libnymea-mqtt server client tests server.depends = libnymea-mqtt +client.depends = libnymea-mqtt tests.depends = libnymea-mqtt diff --git a/server/authorizer.cpp b/server/authorizer.cpp new file mode 100644 index 0000000..6bba335 --- /dev/null +++ b/server/authorizer.cpp @@ -0,0 +1,126 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "authorizer.h" + +#include +#include + +Authorizer::Authorizer(const QString &policyFile, QObject *parent): + QObject{parent}, + m_settingsFile(policyFile) +{ + if (QFile::exists(policyFile)) { + qInfo() << "Using policy file:" << policyFile; + } + +} + +Mqtt::ConnectReturnCode Authorizer::authorizeConnect(int serverAddressId, const QString &clientId, const QString &username, const QString &password, const QHostAddress &peerAddress) +{ + Q_UNUSED(serverAddressId) + Q_UNUSED(peerAddress); + + if (!QFile::exists(m_settingsFile)) { + return Mqtt::ConnectReturnCodeServerUnavailable; + } + MqttPolicy policy = loadPolicy(clientId); + if (!policy.isValid()) { + return Mqtt::ConnectReturnCodeNotAuthorized; + } + if (policy.username() != username || policy.password() != password) { + return Mqtt::ConnectReturnCodeBadUsernameOrPassword; + } + return Mqtt::ConnectReturnCodeAccepted; +} + +bool Authorizer::authorizeSubscribe(int serverAddressId, const QString &clientId, const QString &topicFilter) +{ + Q_UNUSED(serverAddressId) + + qCritical() << "sub filters" << topicFilter; + if (!QFile::exists(m_settingsFile)) { + return false; + } + MqttPolicy policy = loadPolicy(clientId); + if (!policy.isValid()) { + return false; + } + qCritical() << "policy" << policy.allowedSubscribeTopicFilters(); + if (policy.allowedSubscribeTopicFilters().contains(topicFilter)) { + return true; + } + return false; +} + +bool Authorizer::authorizePublish(int serverAddressId, const QString &clientId, const QString &topic) +{ + Q_UNUSED(serverAddressId) + + if (!QFile::exists(m_settingsFile)) { + return false; + } + MqttPolicy policy = loadPolicy(clientId); + if (!policy.isValid()) { + return false; + } + if (policy.allowedPublishTopicFilters().contains(topic)) { + return true; + } + return false; +} + +void Authorizer::addPolicy(const QString &clientId, const QString &username, const QString &password, const QStringList &allowedSubscribeTopicFilters, const QStringList &allowedPublishTopicFilters) +{ + QSettings settings(m_settingsFile, QSettings::IniFormat); + settings.beginGroup(clientId); + settings.setValue("username", username); + settings.setValue("password", password); + settings.setValue("allowedSubscribeTopicFilters", allowedSubscribeTopicFilters); + settings.setValue("allowedPublishTopicFilters", allowedPublishTopicFilters); +} + +void Authorizer::removePolicy(const QString &clientId) +{ + QSettings settings(m_settingsFile, QSettings::IniFormat); + settings.remove(clientId); +} + +MqttPolicy Authorizer::loadPolicy(const QString &clientId) +{ + QSettings settings(m_settingsFile, QSettings::IniFormat); + if (!settings.childGroups().contains(clientId)) { + return MqttPolicy(); + } + settings.beginGroup(clientId); + MqttPolicy policy(clientId, + settings.value("username").toString(), + settings.value("password").toString(), + settings.value("allowedSubscribeTopicFilters").toStringList(), + settings.value("allowedPublishTopicFilters").toStringList()); + return policy; +} diff --git a/server/authorizer.h b/server/authorizer.h new file mode 100644 index 0000000..2b3431e --- /dev/null +++ b/server/authorizer.h @@ -0,0 +1,59 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef AUTHORIZER_H +#define AUTHORIZER_H + +#include "mqttpolicy.h" + +#include + +#include + + +class Authorizer : public QObject, public MqttAuthorizer +{ + Q_OBJECT +public: + explicit Authorizer(const QString &policyFile, QObject *parent = nullptr); + + Mqtt::ConnectReturnCode authorizeConnect(int serverAddressId, const QString &clientId, const QString &username, const QString &password, const QHostAddress &peerAddress) override; + bool authorizeSubscribe(int serverAddressId, const QString &clientId, const QString &topicFilter) override; + bool authorizePublish(int serverAddressId, const QString &clientId, const QString &topic) override; + + void addPolicy(const QString &clientId, const QString &username, const QString &password, const QStringList &allowedSubscribeTopicFilters, const QStringList &allowedPublishTopicFilters); + void removePolicy(const QString &clientId); + +private: + MqttPolicy loadPolicy(const QString &clientId); + +private: + QString m_settingsFile; + +}; + +#endif // AUTHORIZER_H diff --git a/server/certificateloader.cpp b/server/certificateloader.cpp new file mode 100644 index 0000000..5720962 --- /dev/null +++ b/server/certificateloader.cpp @@ -0,0 +1,215 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "certificateloader.h" + +#include + +#include +#include +#include +#include +#include + +// Disabling deprecation warnings for openssl 3.0 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +CertificateLoader::CertificateLoader() +{ + +} + +bool CertificateLoader::loadCertificate(const QString &certificateKeyFileName, const QString &certificateFileName) +{ + QFile certificateKeyFile(certificateKeyFileName); + if (!certificateKeyFile.exists()) { + qWarning() << "Could not load certificate key file" << certificateKeyFile.fileName() << "because the file does not exist."; + return false; + } + + if (!certificateKeyFile.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open" << certificateKeyFile.fileName() << ":" << certificateKeyFile.errorString(); + return false; + } + + m_certificateKey = QSslKey(&certificateKeyFile, QSsl::Rsa); + if (m_certificateKey.isNull()) { + qWarning() << "SSL certificate key" << certificateFileName << "is not valid."; + return false; + } + + qDebug() << "Loaded private certificate key" << certificateKeyFileName; + certificateKeyFile.close(); + + QFile certificateFile(certificateFileName); + if (!certificateFile.exists()) { + qWarning() << "Could not load certificate file" << certificateFile.fileName() << "because the file does not exist."; + return false; + } + + if (!certificateFile.open(QIODevice::ReadOnly)) { + qWarning() << "Could not open" << certificateFile.fileName() << ":" << certificateFile.errorString(); + return false; + } + + m_certificate = QSslCertificate(&certificateFile); + if (m_certificate.isNull()) { + qWarning() << "SSL certificate" << certificateFileName << "is not valid.";; + return false; + } + + qDebug() << "Loaded certificate file" << certificateFileName; + certificateFile.close(); + + return true; +} + +bool CertificateLoader::generateCertificate(const QString &certificateKeyFileName, const QString &certificateFileName) +{ + EVP_PKEY * pkey = nullptr; + BIGNUM *bne = NULL; + RSA * rsa = nullptr; + X509 * x509 = nullptr; + X509_NAME * name = nullptr; + BIO * bp_public = nullptr, * bp_private = nullptr; + const char * keyBuffer = nullptr; + const char * certBuffer = nullptr; + + QFileInfo certFi(certificateFileName); + QFileInfo keyFi(certificateKeyFileName); + + QDir dir; + if (!dir.mkpath(certFi.absolutePath()) || !dir.mkpath(keyFi.absolutePath())) { + qWarning() << "Error creating SSL certificate destination folder"; + return false; + } + + QSaveFile certfile(certificateFileName); + QSaveFile keyFile(certificateKeyFileName); + if (!certfile.open(QFile::WriteOnly | QFile::Truncate | QFile::Unbuffered) || !keyFile.open(QFile::WriteOnly | QFile::Truncate | QFile::Unbuffered)) { + qWarning() << "Error opening SSL certificate files"; + return false; + } + + bne = BN_new(); + BN_set_word(bne, RSA_F4); + q_check_ptr(bne); + + rsa = RSA_new(); + RSA_generate_key_ex(rsa, 2048, bne, nullptr); + q_check_ptr(rsa); + + pkey = EVP_PKEY_new(); + q_check_ptr(pkey); + + EVP_PKEY_assign_RSA(pkey, rsa); + x509 = X509_new(); + q_check_ptr(x509); + // Randomize serial number in case a previous one is stuck in a browser (Chromium + // completely rejects reused serial numbers and doesn't even allow to bypass it by an exception) + qsrand(QUuid::createUuid().toString().remove(QRegExp("[a-zA-Z{}-]")).left(5).toInt()); + ASN1_INTEGER_set(X509_get_serialNumber(x509), qrand()); + X509_gmtime_adj(X509_get_notBefore(x509), 0); // not before current time + X509_gmtime_adj(X509_get_notAfter(x509), 31536000L*10); // not after 10 years from this point + X509_set_pubkey(x509, pkey); + name = X509_get_subject_name(x509); + q_check_ptr(name); + X509_NAME_add_entry_by_txt(name, "E", MBSTRING_ASC, (unsigned char *)"nymea", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"nymea.io", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "OU", MBSTRING_ASC, (unsigned char *)"home", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"nymea GmbH", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "L", MBSTRING_ASC, (unsigned char *)"Vienna", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"AT", -1, -1, 0); + X509_set_issuer_name(x509, name); + X509_sign(x509, pkey, EVP_sha256()); + bp_private = BIO_new(BIO_s_mem()); + q_check_ptr(bp_private); + if(PEM_write_bio_PrivateKey(bp_private, pkey, nullptr, nullptr, 0, nullptr, nullptr) != 1) + { + BN_free(bne); + EVP_PKEY_free(pkey); + X509_free(x509); + BIO_free_all(bp_private); + qWarning() << "PEM_write_bio_PrivateKey"; + return false; + } + bp_public = BIO_new(BIO_s_mem()); + q_check_ptr(bp_public); + if(PEM_write_bio_X509(bp_public, x509) != 1) + { + + BN_free(bne); + EVP_PKEY_free(pkey); + X509_free(x509); + BIO_free_all(bp_public); + BIO_free_all(bp_private); + qWarning() << "PEM_write_bio_X509"; + return false; + } + long pubSize = BIO_get_mem_data(bp_public, &certBuffer); + q_check_ptr(certBuffer); + + long privSize = BIO_get_mem_data(bp_private, &keyBuffer); + q_check_ptr(keyBuffer); + + if (certfile.write(certBuffer, pubSize) == pubSize && keyFile.write(keyBuffer, privSize) == privSize) { + certfile.commit(); + keyFile.commit(); + qDebug() << "Generated new SSL certificate"; + } else { + qWarning() << "Error writing SSL certificate files" << certificateKeyFileName << certificateFileName; + certfile.cancelWriting(); + keyFile.cancelWriting(); + BN_free(bne); + EVP_PKEY_free(pkey); // this will also free the rsa key + X509_free(x509); + BIO_free_all(bp_public); + BIO_free_all(bp_private); + return false; + } + + BN_free(bne); + EVP_PKEY_free(pkey); // this will also free the rsa key + X509_free(x509); + BIO_free_all(bp_public); + BIO_free_all(bp_private); + + return true; +} + +QSslKey CertificateLoader::certificateKey() const +{ + return m_certificateKey; +} + +QSslCertificate CertificateLoader::certificate() const +{ + return m_certificate; +} + +#pragma GCC diagnostic pop diff --git a/server/certificateloader.h b/server/certificateloader.h new file mode 100644 index 0000000..9db44f0 --- /dev/null +++ b/server/certificateloader.h @@ -0,0 +1,51 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef CERTIFICATELOADER_H +#define CERTIFICATELOADER_H + +#include +#include +#include + +class CertificateLoader +{ +public: + CertificateLoader(); + + bool loadCertificate(const QString &certificateKeyFileName, const QString &certificateFileName); + bool generateCertificate(const QString &certificateKeyFileName, const QString &certificateFileName); + + QSslKey certificateKey() const; + QSslCertificate certificate() const; + +private: + QSslKey m_certificateKey; + QSslCertificate m_certificate; +}; + +#endif // CERTIFICATELOADER_H diff --git a/server/main.cpp b/server/main.cpp index 6f8448a..7899b9a 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -25,16 +25,126 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -#include - #include "mqttserver.h" +#include "authorizer.h" +#include "certificateloader.h" + +#include +#include +#include +#include +#include int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); + QString defaultConfigFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/nymea/nymea-mqtt-server.conf"; + QString defaultPolicyFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/nymea/mqttpolicies.conf"; + quint16 defaultTcpPort = 1883; + quint16 defaultWsPort = 0; + bool useSslDefault = false; + QString defaultCertKeyFileName = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/certs/certificate.key"; + QString defaultCertFileName = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/certs/certificate.crt"; + + QCommandLineParser parser; + parser.addOptions({ + {{"config", "c"}, QString("The configuration file to use (default: %1").arg(defaultConfigFile), "file", defaultConfigFile}, + {{"policy-file", "p"}, QString("The policy configuration file to use (default: %1)").arg(defaultPolicyFile), "file", defaultPolicyFile}, + {{"insecure", "i"}, "Run in insecure mode (allow all connections, publishes and subscribes)"}, + {{"tcp-port", "t"}, QString("The port for the TCP server (default: %1, 0 to disable)").arg(defaultTcpPort), "port", QString::number(defaultTcpPort)}, + {{"ws-port", "w"}, "The port for the web socket server (default: disabled)", "port", QString::number(defaultWsPort)}, + {{"add-policy", "a"}, "Add a new client policy"}, + {{"remove-policy", "r"}, "Remove a client policy", "clientId"}, + {{"ssl", "S"}, "Enable SSL encryption (default: disabled)"}, + {{"certificate", "C"}, QString("The SSL certificate to use (default: %1)").arg(defaultCertFileName), "crt file", defaultCertFileName}, + {{"certificate-key", "K"}, QString("The SSL certificate key to use (default: %1)").arg(defaultCertKeyFileName), "key file", defaultCertKeyFileName}, + }); + parser.setApplicationDescription("nymea-mqtt-server is a standalone MQTT broker with support for TCP and web socket connections.\n\n" + "Every command line argument which can be passed, can also be set into the configuration file by specifing the long name for it followed by = and the desired value." + "For example:\n\ntcp-port=1883\nssl=true\n\n" + "Note that any passed command line arguments will still override any values set in the configuration file.\n\n" + "Enabling SSL requires an SSL ertificate which can be configured with the certificate and certificate-key options. If no certificate is found in the given locations, a new self-signed certificate will be generated.\n\n" + "Invoking the application with \"add-policy\" or \"remove-policy\" will allow changing the policies at run time, no broker restart is required. However, existing clients won't be disconnected immediately when a policy is removed but subsequent connect, subscribe or publish operations will be blocked."); + parser.addHelpOption(); + + parser.process(a.arguments()); + + qDebug() << "Using configuration file:" << parser.value("config"); + QSettings settings(parser.value("config"), QSettings::IniFormat); + QString policyFile = parser.isSet("policy-file") ? parser.value("policy-file") : settings.value("policy-file", defaultPolicyFile).toString(); + bool insecure = parser.isSet("insecure") ? true : settings.value("insecure", false).toBool(); + quint16 tcpPort = parser.isSet("tcp-port") ? parser.value("tcp-port").toUInt() : settings.value("tcp-port", defaultTcpPort).toUInt(); + quint16 wsPort = parser.isSet("ws-port") ? parser.value("ws-port").toUInt() : settings.value("ws-port", defaultWsPort).toUInt(); + bool useSsl = parser.isSet("ssl") || settings.value("ssl", useSslDefault).toBool(); + QString certificateKeyFile = parser.isSet("certificate-key") ? parser.value("certificate-key") : settings.value("certificate-key", defaultCertKeyFileName).toString(); + QString certificateFile = parser.isSet("certificate") ? parser.value("certificate") : settings.value("certificate", defaultCertFileName).toString(); + + if (parser.isSet("add-policy")) { + Authorizer authorizer(policyFile); + std::string line; + std::cout << "Client ID: "; + std::getline(std::cin, line); + QString clientId = QString::fromStdString(line); + std::cout << "Username: "; + std::getline(std::cin, line); + QString username = QString::fromStdString(line); + std::cout << "Password: "; + std::getline(std::cin, line); + QString password = QString::fromStdString(line); + std::cout << "Subscribe topic filters (comma separated): "; + std::getline(std::cin, line); + QStringList allowedSubscribeTopicFilters = QString::fromStdString(line).split(","); + std::cout << "Publish topic filters (comma separated): "; + std::getline(std::cin, line); + QStringList allowedPublishTopicFilters = QString::fromStdString(line).split(","); + authorizer.addPolicy(clientId, username, password, allowedSubscribeTopicFilters, allowedPublishTopicFilters); + exit(EXIT_SUCCESS); + } + if (parser.isSet("remove-policy")) { + Authorizer authorizer(policyFile); + authorizer.removePolicy(parser.value("remove-policy")); + exit(EXIT_SUCCESS); + } + MqttServer server; - server.listen(QHostAddress::AnyIPv4, 1883); + + Authorizer *authorizer = nullptr; + if (!insecure) { + authorizer = new Authorizer(policyFile); + server.setAuthorizer(authorizer); + } + + QSslConfiguration sslConfiguration; + if (useSsl) { + CertificateLoader certLoader; + bool loaded = certLoader.loadCertificate(certificateKeyFile, certificateFile); + if (!loaded) { + certLoader.generateCertificate(certificateKeyFile, certificateFile); + loaded = certLoader.loadCertificate(certificateKeyFile, certificateFile); + } + if (!loaded) { + qCritical() << "Certificate files not found and unable to generate a new one."; + exit(EXIT_FAILURE); + } + sslConfiguration.setProtocol(QSsl::TlsV1_2OrLater); + sslConfiguration.setPrivateKey(certLoader.certificateKey()); + sslConfiguration.setLocalCertificate(certLoader.certificate()); + } + + if (tcpPort != 0) { + int serverId = server.listen(QHostAddress::AnyIPv4, tcpPort, sslConfiguration); + if (serverId == -1) { + exit(EXIT_FAILURE); + } + } + + if (wsPort != 0) { + int serverId = server.listenWebSocket(QHostAddress::AnyIPv4, wsPort, sslConfiguration); + if (serverId == -1) { + exit(EXIT_FAILURE); + } + } return a.exec(); } diff --git a/server/mqttpolicy.cpp b/server/mqttpolicy.cpp new file mode 100644 index 0000000..e7faa17 --- /dev/null +++ b/server/mqttpolicy.cpp @@ -0,0 +1,73 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttpolicy.h" + +MqttPolicy::MqttPolicy() +{ + +} + +MqttPolicy::MqttPolicy(const QString &clientId, const QString &username, const QString &password, const QStringList &allowedSubscribeTopicFilters, const QStringList &allowedPublishTopicFilters): + m_clientId(clientId), + m_username(username), + m_password(password), + m_allowedSubscribeTopicFilters(allowedSubscribeTopicFilters), + m_allowedPublishTopicFilters(allowedPublishTopicFilters) +{ + +} + +QString MqttPolicy::clientId() const +{ + return m_clientId; +} + +QString MqttPolicy::username() const +{ + return m_username; +} + +QString MqttPolicy::password() const +{ + return m_password; +} + +QStringList MqttPolicy::allowedSubscribeTopicFilters() const +{ + return m_allowedSubscribeTopicFilters; +} + +QStringList MqttPolicy::allowedPublishTopicFilters() const +{ + return m_allowedPublishTopicFilters; +} + +bool MqttPolicy::isValid() const +{ + return !m_clientId.isEmpty(); +} diff --git a/server/mqttpolicy.h b/server/mqttpolicy.h new file mode 100644 index 0000000..8def10d --- /dev/null +++ b/server/mqttpolicy.h @@ -0,0 +1,56 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under +* the terms of the GNU General Public License as published by the Free Software Foundation, +* GNU version 3. this project 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 project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MQTTPOLICY_H +#define MQTTPOLICY_H + +#include +#include + +class MqttPolicy +{ +public: + MqttPolicy(); + MqttPolicy(const QString &clientId, const QString &username, const QString &password, const QStringList &allowedSubscribeTopicFilters, const QStringList &allowedPublishTopicFilters); + + QString clientId() const; + QString username() const; + QString password() const; + QStringList allowedSubscribeTopicFilters() const; + QStringList allowedPublishTopicFilters() const; + + bool isValid() const; + +private: + QString m_clientId; + QString m_username; + QString m_password; + QStringList m_allowedSubscribeTopicFilters; + QStringList m_allowedPublishTopicFilters; +}; + +#endif // MQTTPOLICY_H diff --git a/server/server.pro b/server/server.pro index 6894291..47b3806 100644 --- a/server/server.pro +++ b/server/server.pro @@ -1,5 +1,5 @@ TEMPLATE = app -TARGET = nymea-mqttserver +TARGET = nymea-mqtt-server include(../nymea-mqtt.pri) @@ -8,9 +8,18 @@ QT -= gui INCLUDEPATH += $$top_srcdir/libnymea-mqtt/ -SOURCES += main.cpp +HEADERS += \ + authorizer.h \ + certificateloader.h \ + mqttpolicy.h -LIBS += -L$$top_builddir/libnymea-mqtt/ -lnymea-mqtt +SOURCES += main.cpp \ + authorizer.cpp \ + certificateloader.cpp \ + mqttpolicy.cpp -target.path = /usr/bin/ +LIBS += -L$$top_builddir/libnymea-mqtt/ -lnymea-mqtt -lssl -lcrypto + +target.path = $$[QT_INSTALL_PREFIX]/bin INSTALLS += target + diff --git a/tests/operation/operation.pro b/tests/common/common.pri similarity index 63% rename from tests/operation/operation.pro rename to tests/common/common.pri index ad286d3..0a123fe 100644 --- a/tests/operation/operation.pro +++ b/tests/common/common.pri @@ -1,4 +1,4 @@ -QT += testlib network +QT += testlib network websockets QT -= gui CONFIG += qt console warn_on depend_includepath testcase @@ -11,6 +11,8 @@ include(../../nymea-mqtt.pri) INCLUDEPATH += $$top_srcdir/libnymea-mqtt/ -SOURCES += test_operation.cpp +HEADERS += $${top_srcdir}/tests/common/mqtttests.h + +SOURCES += $${top_srcdir}/tests/common/mqtttests.cpp LIBS += -L$$top_builddir/libnymea-mqtt/ -lnymea-mqtt diff --git a/tests/operation/test_operation.cpp b/tests/common/mqtttests.cpp similarity index 85% rename from tests/operation/test_operation.cpp rename to tests/common/mqtttests.cpp index 7bacaba..9ecd44d 100644 --- a/tests/operation/test_operation.cpp +++ b/tests/common/mqtttests.cpp @@ -32,81 +32,11 @@ #include #include - -class OperationTests: public QObject -{ - Q_OBJECT +#include "mqtttests.h" #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) -private slots: - void initTestCase(); - void cleanup(); - void connectAndDisconnect(); - void keepAliveTimesOut(); - - void subscribeAndPublish_data(); - void subscribeAndPublish(); - - void willIsSentOnClientDisappearing(); - void willIsNotSentOnClientDisconnecting(); - - void testWillRetain(); - - void testAutoReconnect(); - - void testQoS1Retransmissions(); - - void testMultiSubscription(); - - void testSubscriptionTopicFilters_data(); - void testSubscriptionTopicFilters(); - - void testSubscriptionTopicMatching_data(); - void testSubscriptionTopicMatching(); - - void testSessionManagementDropOldSession(); - void testSessionManagementResumeOldSession(); - void testSessionManagementFailResumeOldSession(); - - void testQoS1PublishToServerIsAckedOnSessionResume(); - void testQoS1PublishToClientIsDeliveredOnSessionResume(); - - void testQoS2PublishToServerIsCompletedOnSessionResume(); - - void testQoS2PublishToClientIsCompletedOnSessionResume(); - - void testRetain(); - - void testUnsubscribe(); - - void testEmptyClientId(); - - void testBinaryPaylaod(); - -private: - // Connects and waits for the MQTT CONNECT to be finished - MqttClient *connectAndWait(const QString &clientId, bool cleanSession = true, quint16 keepAlive = 300, const QString &willTopic = QString(), const QString &willMessage = QString(), Mqtt::QoS willQoS = Mqtt::QoS0, bool willRetain = false); - - // Just connects, returns the client and signalspy which has been created before calling connect. You must delete the spy yourself! - QPair connectToServer(const QString &clientId, bool cleanSession = true, quint16 keepAlive = 300, const QString &willTopic = QString(), const QString &willMessage = QString(), Mqtt::QoS willQoS = Mqtt::QoS0, bool willRetain = false); - - void disconnectAndWait(MqttClient* client); - - bool subscribeAndWait(MqttClient* client, const QString &topic, Mqtt::QoS qos = Mqtt::QoS1); - -private: - QString m_serverHost = "127.0.0.1"; - quint16 m_serverPort = 5555; - MqttServer *m_server = nullptr; - - QList m_clients; -#endif -}; - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) - -MqttClient *OperationTests::connectAndWait(const QString &clientId, bool cleanSession, quint16 keepAlive, const QString &willTopic, const QString &willMessage, Mqtt::QoS willQoS, bool willRetain) +MqttClient *MqttTests::connectAndWait(const QString &clientId, bool cleanSession, quint16 keepAlive, const QString &willTopic, const QString &willMessage, Mqtt::QoS willQoS, bool willRetain) { QPair result = connectToServer(clientId, cleanSession, keepAlive, willTopic, willMessage, willQoS, willRetain); if (result.second->count() == 0) { @@ -120,7 +50,7 @@ MqttClient *OperationTests::connectAndWait(const QString &clientId, bool cleanSe return result.first; } -QPair OperationTests::connectToServer(const QString &clientId, bool cleanSession, quint16 keepAlive, const QString &willTopic, const QString &willMessage, Mqtt::QoS willQoS, bool willRetain) +QPair MqttTests::connectToServer(const QString &clientId, bool cleanSession, quint16 keepAlive, const QString &willTopic, const QString &willMessage, Mqtt::QoS willQoS, bool willRetain) { MqttClient* client = new MqttClient(clientId, keepAlive, willTopic, willMessage.toUtf8(), willQoS, willRetain, this); client->setAutoReconnect(false); @@ -128,11 +58,13 @@ QPair OperationTests::connectToServer(const QString &c m_clients.append(client); QSignalSpy *spy = new QSignalSpy(client, &MqttClient::connected); - client->connectToHost(m_serverHost, m_serverPort, cleanSession); + + connectClientToServer(client, cleanSession); + return qMakePair(client, spy); } -void OperationTests::disconnectAndWait(MqttClient* client) +void MqttTests::disconnectAndWait(MqttClient* client) { QSignalSpy disconnectedSpy(client, &MqttClient::disconnected); client->disconnectFromHost(); @@ -141,7 +73,7 @@ void OperationTests::disconnectAndWait(MqttClient* client) } } -bool OperationTests::subscribeAndWait(MqttClient* client, const QString &topic, Mqtt::QoS qos) +bool MqttTests::subscribeAndWait(MqttClient* client, const QString &topic, Mqtt::QoS qos) { QSignalSpy subscribedSpy(client, &MqttClient::subscribeResult); quint16 packetId = client->subscribe(topic, qos); @@ -152,24 +84,18 @@ bool OperationTests::subscribeAndWait(MqttClient* client, const QString &topic, return subscribedSpy.count() == 1 && subscribedSpy.first().at(0).toInt() == packetId && subscribedSpy.first().at(1).value().first() == expectedSubscribeReturnCode; } -void OperationTests::initTestCase() +void MqttTests::initTestCase() { // QLoggingCategory::setFilterRules("nymea.mqtt.protocol.debug=false"); m_server = new MqttServer(this); - bool registered = false; - quint16 attempts = 0; - do { - registered = m_server->listen(QHostAddress(m_serverHost), m_serverPort + attempts); - } while(!registered && attempts++ < 20); + m_serverId = startServer(m_server); - QVERIFY2(registered, QString("Failed to register server on %1 from port %2 to %3. Tests won't work.").arg(m_serverHost).arg(m_serverPort).arg(m_serverPort+attempts).toUtf8().data()); - - m_serverPort += attempts; + QVERIFY2(m_serverId >= 0, "Failed to register server. Tests won't work."); } -void OperationTests::cleanup() +void MqttTests::cleanup() { while (!m_clients.isEmpty()) { MqttClient *client = m_clients.takeFirst(); @@ -179,7 +105,13 @@ void OperationTests::cleanup() QTRY_COMPARE(m_server->clients().count(), 0); } -void OperationTests::connectAndDisconnect() +void MqttTests::cleanupTestCase() +{ + m_server->close(m_serverId); + delete m_server; +} + +void MqttTests::connectAndDisconnect() { QSignalSpy serverSpy(m_server, &MqttServer::clientConnected); @@ -206,7 +138,7 @@ void OperationTests::connectAndDisconnect() QVERIFY2(serverSpyDisconnect.at(0).first() == clientId, "ClientId not matching on server side."); } -void OperationTests::keepAliveTimesOut() +void MqttTests::keepAliveTimesOut() { QSignalSpy keepAliveSpy(m_server, &MqttServer::clientAlive); MqttClient *client = connectAndWait("keepAlive1sec-client", true, 1); @@ -228,7 +160,7 @@ void OperationTests::keepAliveTimesOut() QVERIFY2(!client->isConnected(), "Client connection still alive but it should have been dropped"); } -void OperationTests::subscribeAndPublish_data() +void MqttTests::subscribeAndPublish_data() { QTest::addColumn("qosClient1"); QTest::addColumn("qosClient2"); @@ -249,7 +181,7 @@ void OperationTests::subscribeAndPublish_data() } } -void OperationTests::subscribeAndPublish() +void MqttTests::subscribeAndPublish() { QFETCH(Mqtt::QoS, qosClient1); QFETCH(Mqtt::QoS, qosClient2); @@ -298,7 +230,7 @@ void OperationTests::subscribeAndPublish() } -void OperationTests::willIsSentOnClientDisappearing() +void MqttTests::willIsSentOnClientDisappearing() { MqttClient *client1 = connectAndWait("subWill-client"); MqttClient *client2 = connectAndWait("pubWill-client", true, 300, "/testtopic", "Bye bye"); @@ -307,14 +239,14 @@ void OperationTests::willIsSentOnClientDisappearing() QVERIFY(subscribeAndWait(client1, "#")); - client2->d_ptr->socket->abort(); + client2->d_ptr->transport->abort(); QTRY_VERIFY2(publishSpy.count() == 1, "Will has not been sent"); QVERIFY2(publishSpy.first().at(0) == "/testtopic", "Will topic not matching"); QVERIFY2(publishSpy.first().at(1) == "Bye bye", "Will message not matching"); } -void OperationTests::willIsNotSentOnClientDisconnecting() +void MqttTests::willIsNotSentOnClientDisconnecting() { MqttClient *client1 = connectAndWait("subWill-client"); MqttClient *client2 = connectAndWait("pubWill-client", true, 300, "/testtopic", "Bye bye"); @@ -331,7 +263,7 @@ void OperationTests::willIsNotSentOnClientDisconnecting() QVERIFY2(publishSpy.count() == 0, "Will has been sent but it should not have been"); } -void OperationTests::testWillRetain() +void MqttTests::testWillRetain() { MqttClient *client1 = connectAndWait("subWill-client"); MqttClient *client2 = connectAndWait("pubWill-client", true, 300, "/testtopic", "Bye bye", Mqtt::QoS1, true); @@ -343,7 +275,7 @@ void OperationTests::testWillRetain() subscribeSpy.wait(); client2->setAutoReconnect(false); - client2->d_ptr->socket->abort(); + client2->d_ptr->transport->abort(); QTRY_VERIFY2(publishSpy.count() == 1, "Will has not been sent"); QVERIFY2(publishSpy.first().at(0) == "/testtopic", QString("Will topic not matching: %1").arg(publishSpy.first().at(0).toString()).toUtf8().data()); @@ -365,7 +297,7 @@ void OperationTests::testWillRetain() QTRY_VERIFY2(clearRetainSpy.count() == 1, "Clearing retain message did not succeed"); } -void OperationTests::testAutoReconnect() +void MqttTests::testAutoReconnect() { MqttClient *client1 = connectAndWait("client1"); client1->setAutoReconnect(true); @@ -373,13 +305,13 @@ void OperationTests::testAutoReconnect() QSignalSpy disconnectedSpy(client1, &MqttClient::disconnected); QSignalSpy connectedSpy(client1, &MqttClient::connected); - client1->d_ptr->socket->abort(); + client1->d_ptr->transport->abort(); QTRY_VERIFY2(disconnectedSpy.count() == 1, "client did not emit disconnected"); QTRY_VERIFY2(connectedSpy.count() == 1, "client did not emit connected"); } -void OperationTests::testQoS1Retransmissions() +void MqttTests::testQoS1Retransmissions() { QSignalSpy serverSpy(m_server, &MqttServer::publishReceived); @@ -388,9 +320,9 @@ void OperationTests::testQoS1Retransmissions() // publish a packet, flush the pipe and immediately drop the connection before we have a chance to receive the PUBACK int packetId = client->publish("/testtopic", "Hello world", Mqtt::QoS1); - client->d_ptr->socket->flush(); + client->d_ptr->transport->flush(); QSignalSpy connectedSpy(client, &MqttClient::connected); - client->d_ptr->socket->abort(); + client->d_ptr->transport->abort(); // Wait for it to reconnect, it should then republish the packet connectedSpy.wait(); @@ -407,7 +339,7 @@ void OperationTests::testQoS1Retransmissions() QCOMPARE(serverSpy.at(1).at(3).toString(), QString("Hello world")); } -void OperationTests::testMultiSubscription() +void MqttTests::testMultiSubscription() { MqttClient *client = connectAndWait("subscription-topics"); QSignalSpy subscribedSpy(client, &MqttClient::subscribeResult); @@ -422,7 +354,7 @@ void OperationTests::testMultiSubscription() QCOMPARE(retCodes, subscriptionReturnCodes); } -void OperationTests::testSubscriptionTopicFilters_data() +void MqttTests::testSubscriptionTopicFilters_data() { QTest::addColumn("topicFilter"); QTest::addColumn("subscriptionReturnCode"); @@ -445,7 +377,7 @@ void OperationTests::testSubscriptionTopicFilters_data() QTest::newRow("+/a/#") << "+/a/#" << Mqtt::SubscribeReturnCodeSuccessQoS0; } -void OperationTests::testSubscriptionTopicFilters() +void MqttTests::testSubscriptionTopicFilters() { QFETCH(QString, topicFilter); QFETCH(Mqtt::SubscribeReturnCode, subscriptionReturnCode); @@ -459,7 +391,7 @@ void OperationTests::testSubscriptionTopicFilters() QCOMPARE(retCodes.first(), subscriptionReturnCode); } -void OperationTests::testSubscriptionTopicMatching_data() +void MqttTests::testSubscriptionTopicMatching_data() { QTest::addColumn("topicFilter"); QTest::addColumn("topic"); @@ -518,7 +450,7 @@ void OperationTests::testSubscriptionTopicMatching_data() } } -void OperationTests::testSubscriptionTopicMatching() +void MqttTests::testSubscriptionTopicMatching() { QFETCH(QString, topicFilter); QFETCH(QString, topic); @@ -546,7 +478,7 @@ void OperationTests::testSubscriptionTopicMatching() QVERIFY2(publishReceivedSpy.count() == receivedPublishMessageCount, QString("PublishReceived signal not received the expected amount of time.\nActual: %1\nExpected: %2").arg(publishReceivedSpy.count()).arg(receivedPublishMessageCount).toUtf8().data()); } -void OperationTests::testSessionManagementDropOldSession() +void MqttTests::testSessionManagementDropOldSession() { MqttClient *client1Session1 = connectAndWait("client1"); client1Session1->setAutoReconnect(false); @@ -578,7 +510,7 @@ void OperationTests::testSessionManagementDropOldSession() QVERIFY2(client1PublishReceivedSpy.count() == 0, "Client 1 did receive the publish but it should not have."); } -void OperationTests::testSessionManagementResumeOldSession() +void MqttTests::testSessionManagementResumeOldSession() { MqttClient *client1Session1 = connectAndWait("client1"); client1Session1->setAutoReconnect(false); @@ -609,7 +541,7 @@ void OperationTests::testSessionManagementResumeOldSession() QTRY_VERIFY2(client1PublishReceivedSpy.count() == 1, "Client 1 did not receive the publish but it should have."); } -void OperationTests::testSessionManagementFailResumeOldSession() +void MqttTests::testSessionManagementFailResumeOldSession() { // try to resume non existing session QPair client = connectToServer("client1", false); @@ -619,7 +551,7 @@ void OperationTests::testSessionManagementFailResumeOldSession() QVERIFY2(!client.second->first().at(0).value().testFlag(Mqtt::ConnackFlagSessionPresent), "Session present flag is set while it should not be."); } -void OperationTests::testQoS1PublishToServerIsAckedOnSessionResume() +void MqttTests::testQoS1PublishToServerIsAckedOnSessionResume() { MqttClient *client = connectAndWait("client1", true); client->setAutoReconnect(true); @@ -628,8 +560,8 @@ void OperationTests::testQoS1PublishToServerIsAckedOnSessionResume() QSignalSpy publishedSpy(client, &MqttClient::published); client->publish("/testtopic", "Hello world", Mqtt::QoS1); - client->d_ptr->socket->flush(); - client->d_ptr->socket->abort(); + client->d_ptr->transport->flush(); + client->d_ptr->transport->abort(); QVERIFY2(publishedSpy.count() == 0, "Should not have received the PUBACK yet... Test is bad."); @@ -639,7 +571,7 @@ void OperationTests::testQoS1PublishToServerIsAckedOnSessionResume() } -void OperationTests::testQoS1PublishToClientIsDeliveredOnSessionResume() +void MqttTests::testQoS1PublishToClientIsDeliveredOnSessionResume() { MqttClient *oldClient1 = connectAndWait("client1", true); QSignalSpy subscribedSpy(oldClient1, &MqttClient::subscribeResult); @@ -647,25 +579,22 @@ void OperationTests::testQoS1PublishToClientIsDeliveredOnSessionResume() QTRY_VERIFY(subscribedSpy.count() == 1); // prevent the client from receiving anything - oldClient1->d_ptr->socket->blockSignals(true); + oldClient1->d_ptr->transport->blockSignals(true); - // pbulish something with a second client + // publish something with a second client MqttClient *client2 = connectAndWait("client2"); QSignalSpy publishedSpy(client2, &MqttClient::published); client2->publish("/testtopic", "Hello world", Mqtt::QoS1); QTRY_VERIFY(publishedSpy.count() == 1); // Resume (take over) old session and make sure we got the publish - MqttClient *newClient1 = new MqttClient("client1", this); - m_clients.append(newClient1); // let cleanupTestcase() clean it up + MqttClient *newClient1 = connectToServer("client1", false).first;; QSignalSpy publishReceivedSpy(newClient1, &MqttClient::publishReceived); - newClient1->connectToHost(m_serverHost, m_serverPort, false); - QTRY_VERIFY2(publishReceivedSpy.count() == 1, "Client did not receive publish packet upon session resume"); } -void OperationTests::testQoS2PublishToServerIsCompletedOnSessionResume() +void MqttTests::testQoS2PublishToServerIsCompletedOnSessionResume() { MqttClient *client = connectAndWait("client1", true); client->setAutoReconnect(true); @@ -674,8 +603,8 @@ void OperationTests::testQoS2PublishToServerIsCompletedOnSessionResume() QSignalSpy publishedSpy(client, &MqttClient::published); client->publish("/testtopic", "Hello world", Mqtt::QoS2); - client->d_ptr->socket->flush(); - client->d_ptr->socket->abort(); + client->d_ptr->transport->flush(); + client->d_ptr->transport->abort(); QVERIFY2(publishedSpy.count() == 0, "Should not have received the PUBACK yet... Test is bad."); @@ -684,7 +613,7 @@ void OperationTests::testQoS2PublishToServerIsCompletedOnSessionResume() QTRY_VERIFY2(publishedSpy.count() == 1, "Published signal not emitted after reconnect"); } -void OperationTests::testQoS2PublishToClientIsCompletedOnSessionResume() +void MqttTests::testQoS2PublishToClientIsCompletedOnSessionResume() { MqttClient *oldClient1 = connectAndWait("client1", true); QSignalSpy subscribedSpy(oldClient1, &MqttClient::subscribeResult); @@ -692,7 +621,7 @@ void OperationTests::testQoS2PublishToClientIsCompletedOnSessionResume() QTRY_VERIFY(subscribedSpy.count() == 1); // prevent the client from receiving anything - oldClient1->d_ptr->socket->blockSignals(true); + oldClient1->d_ptr->transport->blockSignals(true); // pbulish something with a second client MqttClient *client2 = connectAndWait("client2"); @@ -701,16 +630,13 @@ void OperationTests::testQoS2PublishToClientIsCompletedOnSessionResume() QTRY_VERIFY(publishedSpy.count() == 1); // Resume (take over) old session and make sure we got the publish - MqttClient *newClient1 = new MqttClient("client1", this); - m_clients.append(newClient1); // let cleanupTestcase() clean it up + MqttClient *newClient1 = connectToServer("client1", false).first; QSignalSpy publishReceivedSpy(newClient1, &MqttClient::publishReceived); - newClient1->connectToHost(m_serverHost, m_serverPort, false); - QTRY_VERIFY2(publishReceivedSpy.count() == 1, "Client did not receive publish packet upon session resume"); } -void OperationTests::testRetain() +void MqttTests::testRetain() { MqttClient *client1 = connectAndWait("client1", true); @@ -798,7 +724,7 @@ void OperationTests::testRetain() } -void OperationTests::testUnsubscribe() +void MqttTests::testUnsubscribe() { MqttClient *client1 = connectAndWait("client1"); QVERIFY(subscribeAndWait(client1, "testtopic")); @@ -830,7 +756,7 @@ void OperationTests::testUnsubscribe() QVERIFY2(publishReceivedSpy.count() == 0, "Received publish packet even though we should not have"); } -void OperationTests::testEmptyClientId() +void MqttTests::testEmptyClientId() { MqttClient *client1 = connectAndWait(""); QVERIFY2(client1->isConnected(), "Client did not connect"); @@ -844,7 +770,7 @@ void OperationTests::testEmptyClientId() QTRY_VERIFY2(client3.first->isConnected() == false, "Connection should have been dropped"); } -void OperationTests::testBinaryPaylaod() +void MqttTests::testBinaryPaylaod() { MqttClient *client = connectAndWait(""); QVERIFY2(client->isConnected(), "Client did not connect"); @@ -858,6 +784,3 @@ void OperationTests::testBinaryPaylaod() } #endif - -QTEST_MAIN(OperationTests) -#include "test_operation.moc" diff --git a/tests/common/mqtttests.h b/tests/common/mqtttests.h new file mode 100644 index 0000000..a81d03f --- /dev/null +++ b/tests/common/mqtttests.h @@ -0,0 +1,108 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttserver.h" +#include "mqttclient.h" +#include "mqttclient_p.h" + +#include +#include + + +class MqttTests: public QObject +{ + Q_OBJECT + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) + +private slots: + void initTestCase(); + void cleanup(); + void cleanupTestCase(); + void connectAndDisconnect(); + void keepAliveTimesOut(); + + void subscribeAndPublish_data(); + void subscribeAndPublish(); + + void willIsSentOnClientDisappearing(); + void willIsNotSentOnClientDisconnecting(); + + void testWillRetain(); + + void testAutoReconnect(); + + void testQoS1Retransmissions(); + + void testMultiSubscription(); + + void testSubscriptionTopicFilters_data(); + void testSubscriptionTopicFilters(); + + void testSubscriptionTopicMatching_data(); + void testSubscriptionTopicMatching(); + + void testSessionManagementDropOldSession(); + void testSessionManagementResumeOldSession(); + void testSessionManagementFailResumeOldSession(); + + void testQoS1PublishToServerIsAckedOnSessionResume(); + void testQoS1PublishToClientIsDeliveredOnSessionResume(); + + void testQoS2PublishToServerIsCompletedOnSessionResume(); + + void testQoS2PublishToClientIsCompletedOnSessionResume(); + + void testRetain(); + + void testUnsubscribe(); + + void testEmptyClientId(); + + void testBinaryPaylaod(); +#endif + +private: + // Connects and waits for the MQTT CONNECT to be finished + MqttClient *connectAndWait(const QString &clientId, bool cleanSession = true, quint16 keepAlive = 300, const QString &willTopic = QString(), const QString &willMessage = QString(), Mqtt::QoS willQoS = Mqtt::QoS0, bool willRetain = false); + + // Just connects, returns the client and signalspy which has been created before calling connect. You must delete the spy yourself! + QPair connectToServer(const QString &clientId, bool cleanSession = true, quint16 keepAlive = 300, const QString &willTopic = QString(), const QString &willMessage = QString(), Mqtt::QoS willQoS = Mqtt::QoS0, bool willRetain = false); + + void disconnectAndWait(MqttClient* client); + + bool subscribeAndWait(MqttClient* client, const QString &topic, Mqtt::QoS qos = Mqtt::QoS1); + + virtual int startServer(MqttServer *server) = 0; + virtual void connectClientToServer(MqttClient *client, bool cleanSession) = 0; + +private: + MqttServer *m_server = nullptr; + int m_serverId = -1; + + QList m_clients; +}; diff --git a/tests/tcp/tcp.pro b/tests/tcp/tcp.pro new file mode 100644 index 0000000..84fc9c8 --- /dev/null +++ b/tests/tcp/tcp.pro @@ -0,0 +1,4 @@ +include(../common/common.pri) + +SOURCES += test_tcp.cpp + diff --git a/tests/tcp/test_tcp.cpp b/tests/tcp/test_tcp.cpp new file mode 100644 index 0000000..9e11dda --- /dev/null +++ b/tests/tcp/test_tcp.cpp @@ -0,0 +1,63 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttserver.h" +#include "mqttclient.h" +#include "mqttclient_p.h" + +#include +#include + +#include "../common/mqtttests.h" + +class TcpTests: public MqttTests +{ + Q_OBJECT + +private: + int startServer(MqttServer *server) override; + void connectClientToServer(MqttClient *client, bool cleanSession) override; + + QString m_serverHost = "127.0.0.1"; + quint16 m_serverPort = 5555; + +}; + +int TcpTests::startServer(MqttServer *server) +{ + return server->listen(QHostAddress(m_serverHost), m_serverPort); +} + +void TcpTests::connectClientToServer(MqttClient *client, bool cleanSession) +{ + qDebug() << "Connecting to TCP"; + client->connectToHost(m_serverHost, m_serverPort, cleanSession); +} + +QTEST_MAIN(TcpTests) + +#include "test_tcp.moc" diff --git a/tests/tests.pro b/tests/tests.pro index 70eb65a..3f8a00c 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,3 +1,3 @@ TEMPLATE = subdirs -SUBDIRS += operation +SUBDIRS += tcp websocket diff --git a/tests/websocket/test_websocket.cpp b/tests/websocket/test_websocket.cpp new file mode 100644 index 0000000..4eba487 --- /dev/null +++ b/tests/websocket/test_websocket.cpp @@ -0,0 +1,68 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by copyright law, and +* remains the property of nymea GmbH. All rights, including reproduction, publication, +* editing and translation, are reserved. The use of this project is subject to the terms of a +* license agreement to be concluded with nymea GmbH in accordance with the terms +* of use of nymea GmbH, available under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the terms of the GNU +* Lesser General Public License as published by the Free Software Foundation; version 3. +* this project 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 Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along with this project. +* If not, see . +* +* For any further details and any questions please contact us under contact@nymea.io +* or see our FAQ/Licensing Information on https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "mqttserver.h" +#include "mqttclient.h" +#include "mqttclient_p.h" + +#include +#include + +#include "../common/mqtttests.h" + +class WebSocketTests: public MqttTests +{ + Q_OBJECT + +private: + int startServer(MqttServer *server) override; + void connectClientToServer(MqttClient *client, bool cleanSession) override; + + QString m_serverHost = "127.0.0.1"; + quint16 m_serverPort = 5556; + +}; + +int WebSocketTests::startServer(MqttServer *server) +{ + return server->listenWebSocket(QHostAddress(m_serverHost), m_serverPort); +} + +void WebSocketTests::connectClientToServer(MqttClient *client, bool cleanSession) +{ + QUrl url; + url.setScheme("ws"); + url.setHost(m_serverHost); + url.setPort(m_serverPort); + QNetworkRequest request(url); + qDebug() << "Connecting to WebSocket"; + client->connectToHost(request, cleanSession); +} + +QTEST_MAIN(WebSocketTests) + +#include "test_websocket.moc" diff --git a/tests/websocket/websocket.pro b/tests/websocket/websocket.pro new file mode 100644 index 0000000..89c7c9d --- /dev/null +++ b/tests/websocket/websocket.pro @@ -0,0 +1,4 @@ +include(../common/common.pri) + +SOURCES += test_websocket.cpp +