diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e31f065ea..3f7f13108 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,7 +36,7 @@ jobs: if: ${{ matrix.language == 'cpp' }} run: | sudo apt-get update - sudo apt-get install --yes git build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev libftdi1-dev + sudo apt-get install --yes git build-essential qtbase5-dev libqt5serialport5-dev libqt5websockets5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev libftdi1-dev - name: Temporarily downgrade CMake to 3.28.3 # Please remove if GitHub has updated Cmake (greater than 3.30.0) uses: jwlawson/actions-setup-cmake@v2 diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml index 7c3cc3963..064155a9a 100644 --- a/.github/workflows/qt5_6.yml +++ b/.github/workflows/qt5_6.yml @@ -191,9 +191,9 @@ jobs: - name: 📥 Install Qt uses: jurplel/install-qt-action@v4 with: - version: ${{ inputs.qt_version == '6' && '6.7' || '5.15.*' }} + version: ${{ inputs.qt_version == '6' && '6.8' || '5.15.*' }} target: 'desktop' - modules: ${{ inputs.qt_version == '6' && 'qtserialport' || '' }} + modules: ${{ inputs.qt_version == '6' && 'qtserialport qtwebsockets' || '' }} cache: 'true' cache-key-prefix: 'cache-qt-windows' diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbc6c900..a4a901b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed: Python 3.12 crashes (#1747) - osX Grabber: Use ScreenCaptureKit under macOS 15 and above - Removed maximum LED number constraint from Matrix layout schema which was not synced with the UI behaviour (#1804) +- Fixed bespoke WebSocket implementation by using of QWebSockets (#1816, #1448, #1247, #1130) **JSON-API** - Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. diff --git a/CMakeLists.txt b/CMakeLists.txt index 50e5faa8e..af7b746e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -570,7 +570,7 @@ message(STATUS "Found Qt Version: ${QT_VERSION}") if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) set(QT_MIN_VERSION "6.2.2") else() - set(QT_MIN_VERSION "5.5.0") + set(QT_MIN_VERSION "5.9.0") endif() if("${QT_VERSION}" VERSION_LESS "${QT_MIN_VERSION}") diff --git a/doc/development/CompileHowto.md b/doc/development/CompileHowto.md index 8338bf31e..dc9104c72 100644 --- a/doc/development/CompileHowto.md +++ b/doc/development/CompileHowto.md @@ -30,16 +30,16 @@ wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/ ### Ubuntu -**amd64 (Jammy):** +**amd64 (Noble Numbat):** ```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh --name jammy +wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh --name noble ``` ### Fedora -**amd64 (39):** +**amd64 (41):** ```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh --name 39 +wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh --name 41 ``` ## Cross compilation on amd64 for developers @@ -61,14 +61,14 @@ cd $HYPERION_HOME ```console sudo apt-get update -sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev libftdi1-dev +sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5websockets5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev libftdi1-dev ``` **Ubuntu (22.04+) - Qt6 based** ```console sudo apt-get update -sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libxkbcommon-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config libftdi1-dev +sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libqt6websockets6-dev libxkbcommon-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config libftdi1-dev ``` **For Linux X11/XCB grabber support** @@ -110,7 +110,7 @@ See [AUR](https://aur.archlinux.org/packages/?O=0&SeB=nd&K=hyperion&outdated=&SB The following dependencies are needed to build hyperion.ng on fedora. ```console sudo dnf -y groupinstall "Development Tools" -sudo dnf install python3-devel qt-devel qt5-qtbase-devel qt5-qtserialport-devel xrandr xcb-util-image-devel qt5-qtx11extras-devel alsa-lib-devel turbojpeg-devel libusb-devel xcb-util-devel dbus-devel openssl-devel fedora-packager rpmdevtools gcc libcec-devel libftdi1-dev +sudo dnf install python3-devel qt-devel qt6-qtbase-devel qt6-qtserialport-devel qt6-qtwebsockets-devel xrandr xcb-util-image-devel qt5-qtx11extras-devel alsa-lib-devel turbojpeg-devel libusb-devel xcb-util-devel dbus-devel openssl-devel fedora-packager rpmdevtools gcc libcec-devel libftdi1-dev ``` After installing the dependencies, you can continue with the compile instructions later on this page (the more detailed way..). diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index ebb399655..96dc365d6 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -932,13 +932,17 @@ void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonA // Add infor about the type of setting elements QJsonObject settingTypes; QJsonArray globalSettingTypes; - for (const QString &type : SettingsTable().getGlobalSettingTypes()) { + + SettingsTable settingsTable; + for (const QString &type : settingsTable.getGlobalSettingTypes()) + { globalSettingTypes.append(type); } settingTypes.insert("globalProperties", globalSettingTypes); QJsonArray instanceSettingTypes; - for (const QString &type : SettingsTable().getInstanceSettingTypes()) { + for (const QString &type : settingsTable.getInstanceSettingTypes()) + { instanceSettingTypes.append(type); } settingTypes.insert("instanceProperties", instanceSettingTypes); diff --git a/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp b/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp index ce168821c..407d9d0cf 100644 --- a/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp +++ b/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp @@ -67,12 +67,12 @@ int ProviderFtdi::open() Debug(_log, "Opening FTDI device=%s", QSTRING_CSTR(_deviceName)); - FTDI_CHECK_RESULT((rc = ftdi_usb_open_string(_ftdic, QSTRING_CSTR(_deviceName))) < 0); + FTDI_CHECK_RESULT((rc = ftdi_usb_open_string(_ftdic, QSTRING_CSTR(_deviceName))) < 0) /* doing this disable resets things if they were in a bad state */ - FTDI_CHECK_RESULT((rc = ftdi_disable_bitbang(_ftdic)) < 0); - FTDI_CHECK_RESULT((rc = ftdi_setflowctrl(_ftdic, SIO_DISABLE_FLOW_CTRL)) < 0); - FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0x00, BITMODE_RESET)) < 0); - FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0xff, BITMODE_MPSSE)) < 0); + FTDI_CHECK_RESULT((rc = ftdi_disable_bitbang(_ftdic)) < 0) + FTDI_CHECK_RESULT((rc = ftdi_setflowctrl(_ftdic, SIO_DISABLE_FLOW_CTRL)) < 0) + FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0x00, BITMODE_RESET)) < 0) + FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0xff, BITMODE_MPSSE)) < 0) double reference_clock = 60e6; int divisor = (reference_clock / 2 / _baudRate_Hz) - 1; @@ -86,7 +86,7 @@ int ProviderFtdi::open() pinDirection }; - FTDI_CHECK_RESULT((rc = ftdi_write_data(_ftdic, buf.data(), buf.size())) != buf.size()); + FTDI_CHECK_RESULT(static_cast(rc = ftdi_write_data(_ftdic, buf.data(), static_cast(buf.size()))) != buf.size()) _isDeviceReady = true; return rc; @@ -134,7 +134,7 @@ int ProviderFtdi::writeBytes(const qint64 size, const uint8_t *data) // SET_BITS_LOW takes 2 arguments, so we're inserting data in -3 position from the end buf.insert(buf.end() - 3, &data[0], &data[size]); - FTDI_CHECK_RESULT((rc = ftdi_write_data(_ftdic, buf.data(), buf.size())) != buf.size()); + FTDI_CHECK_RESULT(static_cast(rc = ftdi_write_data(_ftdic, buf.data(), static_cast(buf.size()))) != buf.size()) return rc; } @@ -152,7 +152,7 @@ QJsonObject ProviderFtdi::discover(const QJsonObject & /*params*/) struct ftdi_device_list *curdev = devlist; QMap deviceIndexes; - while (curdev) + while (curdev != nullptr) { libusb_device_descriptor desc; int rc = libusb_get_device_descriptor(curdev->dev, &desc); @@ -161,8 +161,7 @@ QJsonObject ProviderFtdi::discover(const QJsonObject & /*params*/) QString vendorIdentifier = QString("0x%1").arg(desc.idVendor, 4, 16, QChar{'0'}); QString productIdentifier = QString("0x%1").arg(desc.idProduct, 4, 16, QChar{'0'}); QString vendorAndProduct = QString("%1:%2") - .arg(vendorIdentifier) - .arg(productIdentifier); + .arg(vendorIdentifier,productIdentifier); uint8_t deviceIndex = deviceIndexes.value(vendorAndProduct, 0); char serial_string[128] = {0}; @@ -174,7 +173,7 @@ QJsonObject ProviderFtdi::discover(const QJsonObject & /*params*/) QString ftdiOpenString; if(!serialNumber.isEmpty()) { - ftdiOpenString = QString("s:%1:%2").arg(vendorAndProduct).arg(serialNumber); + ftdiOpenString = QString("s:%1:%2").arg(vendorAndProduct, serialNumber); } else { diff --git a/libsrc/python/PythonProgram.cpp b/libsrc/python/PythonProgram.cpp index 6f0b08c92..942187bc2 100644 --- a/libsrc/python/PythonProgram.cpp +++ b/libsrc/python/PythonProgram.cpp @@ -25,7 +25,7 @@ PythonProgram::PythonProgram(const QString& name, Logger* log) : PyEval_RestoreThread(mainThreadState); _tstate = Py_NewInterpreter(); #else - PyThreadState* prev = PyThreadState_Swap(NULL); + PyThreadState_Swap(NULL); // Create a new interpreter configuration object PyInterpreterConfig config{}; diff --git a/libsrc/utils/ImageResampler.cpp b/libsrc/utils/ImageResampler.cpp index b205e0d30..d36beb105 100644 --- a/libsrc/utils/ImageResampler.cpp +++ b/libsrc/utils/ImageResampler.cpp @@ -50,16 +50,15 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i outputImage.resize(outputWidth, outputHeight); - int xDestStart, xDestEnd; - int yDestStart, yDestEnd; + int xDestStart {0}; + int xDestEnd = {outputWidth-1}; + int yDestStart = {0}; + int yDestEnd = {outputWidth-1}; switch (_flipMode) { case FlipMode::NO_CHANGE: - xDestStart = 0; - xDestEnd = outputWidth-1; - yDestStart = 0; - yDestEnd = outputHeight-1; + //use the initalized values break; case FlipMode::HORIZONTAL: xDestStart = 0; diff --git a/libsrc/webserver/CMakeLists.txt b/libsrc/webserver/CMakeLists.txt index 12e5345f2..70c670522 100644 --- a/libsrc/webserver/CMakeLists.txt +++ b/libsrc/webserver/CMakeLists.txt @@ -1,3 +1,4 @@ +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebSockets REQUIRED) file(GLOB_RECURSE webFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig/*) file(RELATIVE_PATH webConfigPath ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig) @@ -28,13 +29,13 @@ add_library(webserver ${CMAKE_SOURCE_DIR}/libsrc/webserver/StaticFileServing.cpp ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.h ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.cpp - ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketClient.h - ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketClient.cpp - ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketUtils.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketJsonHandler.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketJsonHandler.cpp ${CMAKE_BINARY_DIR}/WebConfig.qrc -) + ) target_link_libraries(webserver + Qt${QT_VERSION_MAJOR}::WebSockets hyperion hyperion-utils hyperion-api diff --git a/libsrc/webserver/QtHttpClientWrapper.cpp b/libsrc/webserver/QtHttpClientWrapper.cpp index ea9aeb0d4..62a3d0daa 100644 --- a/libsrc/webserver/QtHttpClientWrapper.cpp +++ b/libsrc/webserver/QtHttpClientWrapper.cpp @@ -5,7 +5,7 @@ #include "QtHttpReply.h" #include "QtHttpServer.h" #include "QtHttpHeader.h" -#include "WebSocketClient.h" +#include "WebSocketJsonHandler.h" #include "WebJsonRpc.h" #include @@ -27,8 +27,9 @@ QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, const bool& localCo , m_localConnection(localConnection) , m_websocketClient(nullptr) , m_webJsonRpc (nullptr) + , m_websocketServer (nullptr) { - connect (m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); + connect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); } QString QtHttpClientWrapper::getGuid (void) @@ -50,6 +51,11 @@ void QtHttpClientWrapper::onClientDataReceived (void) { if (m_sockClient != Q_NULLPTR) { + if (!m_sockClient->isTransactionStarted()) + { + m_sockClient->startTransaction(); + } + while (m_sockClient->bytesAvailable () != 0) { QByteArray line = m_sockClient->readLine (); @@ -162,22 +168,30 @@ void QtHttpClientWrapper::onClientDataReceived (void) { case RequestParsed: // a valid request has ben fully parsed { - // Catch websocket header "Upgrade" - if(m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower() == "websocket") + const auto& upgradeValue = m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower(); + if (upgradeValue == "websocket") { if(m_websocketClient == Q_NULLPTR) { // disconnect this slot from socket for further requests disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); - // disabling packet bunching - m_sockClient->setSocketOption(QAbstractSocket::LowDelayOption, 1); - m_sockClient->setSocketOption(QAbstractSocket::KeepAliveOption, 1); - m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, m_localConnection, this); + m_sockClient->rollbackTransaction(); + + QString servername = QCoreApplication::applicationName() + QLatin1Char('/') + HYPERION_VERSION; + QWebSocketServer::SslMode secureMode = m_serverHandle->isSecure() ? QWebSocketServer::SecureMode : QWebSocketServer::NonSecureMode; + m_websocketServer.reset(new QWebSocketServer(servername, secureMode)); + connect(m_websocketServer.get(), &QWebSocketServer::newConnection, + this, &QtHttpClientWrapper::onNewWebSocketConnection); + + m_websocketServer->handleConnection(m_sockClient); + emit m_sockClient->readyRead(); + return; } break; } + m_sockClient->commitTransaction(); // add post data to request and catch /jsonrpc subroute url if ( m_currentRequest->getCommand() == "POST") { @@ -227,6 +241,8 @@ void QtHttpClientWrapper::onClientDataReceived (void) case ParsingError: // there was an error durin one of parsing steps { m_sockClient->readAll (); // clear remaining buffer to ignore content + m_sockClient->commitTransaction(); + QtHttpReply reply (m_serverHandle); reply.setStatusCode (QtHttpReply::BadRequest); reply.appendRawData (QByteArrayLiteral ("

Bad Request (HTTP parsing error) !

")); @@ -365,3 +381,17 @@ void QtHttpClientWrapper::closeConnection() } m_sockClient->close (); } + +void QtHttpClientWrapper::onNewWebSocketConnection() { + + // Handle the pending connection + QWebSocket* webSocket = m_websocketServer->nextPendingConnection(); + if (webSocket) { + // Manage the WebSocketJsonHandler for this connection + WebSocketJsonHandler* handler = new WebSocketJsonHandler(webSocket); + connect(webSocket, &QWebSocket::disconnected, handler, &QObject::deleteLater); + } + else { + qWarning() << "No pending WebSocket connection!"; + } +} diff --git a/libsrc/webserver/QtHttpClientWrapper.h b/libsrc/webserver/QtHttpClientWrapper.h index fd2d9f64f..c0dedb629 100644 --- a/libsrc/webserver/QtHttpClientWrapper.h +++ b/libsrc/webserver/QtHttpClientWrapper.h @@ -3,6 +3,9 @@ #include #include +#include +#include +#include class QTcpSocket; @@ -39,8 +42,12 @@ class QtHttpClientWrapper : public QObject { /// void closeConnection(); +signals: + void newWebSocketConnection(); + private slots: void onClientDataReceived (void); + void onNewWebSocketConnection(); protected: ParsingStatus sendReplyToClient (QtHttpReply * reply); @@ -59,6 +66,7 @@ protected slots: WebSocketClient * m_websocketClient; WebJsonRpc * m_webJsonRpc; QByteArray m_fragment; + QScopedPointer m_websocketServer; }; #endif // QTHTTPCLIENTWRAPPER_H diff --git a/libsrc/webserver/QtHttpServer.cpp b/libsrc/webserver/QtHttpServer.cpp index ff8fe207c..24e6b4b7a 100644 --- a/libsrc/webserver/QtHttpServer.cpp +++ b/libsrc/webserver/QtHttpServer.cpp @@ -40,7 +40,7 @@ QtHttpServer::QtHttpServer (QObject * parent) , m_netOrigin (NetOrigin::getInstance()) { m_sockServer = new QtHttpServerWrapper (this); - connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); + connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected, Qt::UniqueConnection); } void QtHttpServer::start (quint16 port) @@ -81,27 +81,27 @@ void QtHttpServer::onClientConnected (void) { if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) { - if(m_netOrigin->accessAllowed(sock->peerAddress(), sock->localAddress())) + if (m_netOrigin->accessAllowed(sock->peerAddress(), sock->localAddress())) { - connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); + connect(sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); if (m_useSsl) { - if (QSslSocket * ssl = qobject_cast (sock)) + if (QSslSocket* ssl = qobject_cast (sock)) { - connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); - connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); - connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); - connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); - ssl->setLocalCertificateChain (m_sslCerts); - ssl->setPrivateKey (m_sslKey); - ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); - ssl->startServerEncryption (); + connect(ssl, SslErrorSignal(&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); + connect(ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); + connect(ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); + connect(ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); + ssl->setLocalCertificateChain(m_sslCerts); + ssl->setPrivateKey(m_sslKey); + ssl->setPeerVerifyMode(QSslSocket::AutoVerifyPeer); + ssl->startServerEncryption(); } } - QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, m_netOrigin->isLocalAddress(sock->peerAddress(), sock->localAddress()), this); - m_socksClientsHash.insert (sock, wrapper); + QtHttpClientWrapper* wrapper = new QtHttpClientWrapper(sock, m_netOrigin->isLocalAddress(sock->peerAddress(), sock->localAddress()), this); + m_socksClientsHash.insert(sock, wrapper); emit clientConnected (wrapper->getGuid ()); } else diff --git a/libsrc/webserver/QtHttpServer.h b/libsrc/webserver/QtHttpServer.h index 4e09c6d7b..67e087e45 100644 --- a/libsrc/webserver/QtHttpServer.h +++ b/libsrc/webserver/QtHttpServer.h @@ -46,21 +46,22 @@ class QtHttpServer : public QObject typedef void (QSslSocket::* SslErrorSignal) (const QList &); - const QString & getServerName (void) const { return m_serverName; }; + const QString & getServerName (void) const { return m_serverName; } - quint16 getServerPort (void) const { return m_sockServer->serverPort(); }; - QString getErrorString (void) const { return m_sockServer->errorString(); }; - bool isListening() { return m_sockServer->isListening(); }; + quint16 getServerPort (void) const { return m_sockServer->serverPort(); } + QString getErrorString (void) const { return m_sockServer->errorString(); } + bool isListening() { return m_sockServer->isListening(); } + bool isSecure() { return m_useSsl; } public slots: void start (quint16 port = 0); void stop (void); - void setUseSecure (const bool ssl = true); - void setServerName (const QString & serverName) { m_serverName = serverName; }; - void setPrivateKey (const QSslKey & key) { m_sslKey = key; }; - void setCertificates (const QList & certs) { m_sslCerts = certs; }; - QSslKey getPrivateKey() { return m_sslKey; }; - QList getCertificates() { return m_sslCerts; }; + void setUseSecure (bool ssl = true); + void setServerName (const QString & serverName) { m_serverName = serverName; } + void setPrivateKey (const QSslKey & key) { m_sslKey = key; } + void setCertificates (const QList & certs) { m_sslCerts = certs; } + QSslKey getPrivateKey() { return m_sslKey; } + QList getCertificates() { return m_sslCerts; } signals: void started (quint16 port); @@ -73,10 +74,10 @@ public slots: private slots: void onClientConnected (void); void onClientDisconnected (void); - void onClientSslEncrypted (void) { }; - void onClientSslPeerVerifyError (const QSslError & err) { Q_UNUSED (err) }; - void onClientSslErrors (const QList & errors) { Q_UNUSED (errors) }; - void onClientSslModeChanged (QSslSocket::SslMode mode) { Q_UNUSED (mode) }; + void onClientSslEncrypted (void) { } + void onClientSslPeerVerifyError (const QSslError & err) { Q_UNUSED (err) } + void onClientSslErrors (const QList & errors) { Q_UNUSED (errors) } + void onClientSslModeChanged (QSslSocket::SslMode mode) { Q_UNUSED (mode) } private: bool m_useSsl; diff --git a/libsrc/webserver/WebSocketClient.cpp b/libsrc/webserver/WebSocketClient.cpp deleted file mode 100644 index a4eab0e3f..000000000 --- a/libsrc/webserver/WebSocketClient.cpp +++ /dev/null @@ -1,359 +0,0 @@ -#include "WebSocketClient.h" -#include "QtHttpRequest.h" -#include "QtHttpHeader.h" - -#include -#include -#include - -#include -#include -#include -#include - -WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, bool localConnection, QObject* parent) - : QObject(parent) - , _socket(sock) - , _log(Logger::getInstance("WEBSOCKET")) -{ - // connect socket; disconnect handled from QtHttpServer - connect(_socket, &QTcpSocket::readyRead , this, &WebSocketClient::handleWebSocketFrame); - - // QtHttpRequest contains all headers for handshake - QByteArray secWebSocketKey = request->getHeader(QtHttpHeader::SecWebSocketKey); - const QString client = request->getClientInfo().clientAddress.toString(); - - // Json processor - _jsonAPI.reset(new JsonAPI(client, _log, localConnection, this)); - connect(_jsonAPI.get(), &JsonAPI::callbackReady, this, &WebSocketClient::sendMessage); - connect(_jsonAPI.get(), &JsonAPI::forceClose, this,[this]() { this->sendClose(CLOSECODE::NORMAL); }); - - connect(_jsonAPI->getCallBack().get(), &JsonCallbacks::callbackReady, this, &WebSocketClient::sendMessage); - - connect(this, &WebSocketClient::handleMessage, _jsonAPI.get(), &JsonAPI::handleMessage); - - Debug(_log, "New connection from %s", QSTRING_CSTR(client)); - - // do handshake - secWebSocketKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - QByteArray hash = QCryptographicHash::hash(secWebSocketKey, QCryptographicHash::Sha1).toBase64(); - - QString data - = QString("HTTP/1.1 101 Switching Protocols\r\n") - + QString("Upgrade: websocket\r\n") - + QString("Connection: Upgrade\r\n") - + QString("Sec-WebSocket-Accept: ")+QString(hash.data()) + "\r\n\r\n"; - - _socket->write(QSTRING_CSTR(data), data.size()); - _socket->flush(); - - // Init JsonAPI - _jsonAPI->initialize(); -} - -void WebSocketClient::handleWebSocketFrame() -{ - while (_socket->bytesAvailable()) - { - // we are on no continious reading from socket from call before - if (!_notEnoughData) - { - getWsFrameHeader(&_wsh); - } - - if(_socket->bytesAvailable() < (qint64)_wsh.payloadLength) - { - _notEnoughData=true; - return; - } - _notEnoughData = false; - - QByteArray buf = _socket->read(_wsh.payloadLength); - - if (OPCODE::invalid((OPCODE::value)_wsh.opCode)) - { - sendClose(CLOSECODE::INV_TYPE, "invalid opcode"); - return; - } - - // check the type of data frame - bool isContinuation=false; - - switch (_wsh.opCode) - { - case OPCODE::CONTINUATION: - isContinuation = true; - // no break here, just jump over to opcode text - - case OPCODE::BINARY: - case OPCODE::TEXT: - { - // A fragmented message consists of a single frame with the FIN bit - // clear and an opcode other than 0, followed by zero or more frames - // with the FIN bit clear and the opcode set to 0, and terminated by - // a single frame with the FIN bit set and an opcode of 0. - // - // Store frame type given by first frame - if (_wsh.opCode != OPCODE::CONTINUATION ) - { - _frameOpCode = _wsh.opCode; - } - - // check for protocol violations - if (_onContinuation && !isContinuation) - { - sendClose(CLOSECODE::VIOLATION, "protocol violation, somebody sends frames in between continued frames"); - return; - } - - if (!_wsh.masked && _wsh.opCode == OPCODE::TEXT) - { - sendClose(CLOSECODE::VIOLATION, "protocol violation, unmasked text frames not allowed"); - return; - } - - // unmask data - for (int i=0; i < buf.size(); i++) - { - buf[i] = buf[i] ^ _wsh.key[i % 4]; - } - - _onContinuation = !_wsh.fin || isContinuation; - - // frame contains text, extract it, append data if this is a continuation - if (_wsh.fin && ! isContinuation) // one frame - { - _wsReceiveBuffer.clear(); - } - _wsReceiveBuffer.append(buf); - - // this is the final frame, decode and handle data - if (_wsh.fin) - { - _onContinuation = false; - - if (_frameOpCode == OPCODE::TEXT) - { - emit handleMessage(QString(_wsReceiveBuffer),""); - } - else - { - handleBinaryMessage(_wsReceiveBuffer); - } - _wsReceiveBuffer.clear(); - - } - } - break; - - case OPCODE::CLOSE: - { - sendClose(CLOSECODE::NORMAL); - } - break; - - case OPCODE::PING: - { - // ping received, send pong - quint8 pong[] = {OPCODE::PONG, 0}; - _socket->write((const char*)pong, 2); - _socket->flush(); - } - break; - - case OPCODE::PONG: - { - Error(_log, "pong received, protocol violation!"); - } - - default: - Warning(_log, "strange %d\n%s\n", _wsh.opCode, QSTRING_CSTR(QString(buf))); - } - } -} - -void WebSocketClient::getWsFrameHeader(WebSocketHeader* header) -{ - char fin_rsv_opcode, mask_length; - _socket->getChar(&fin_rsv_opcode); - _socket->getChar(&mask_length); - - header->fin = (fin_rsv_opcode & BHB0_FIN) == BHB0_FIN; - header->opCode = fin_rsv_opcode & BHB0_OPCODE; - header->masked = (mask_length & BHB1_MASK) == BHB1_MASK; - header->payloadLength = mask_length & BHB1_PAYLOAD; - - // get size of payload - switch (header->payloadLength) - { - case payload_size_code_16bit: - { - QByteArray buf = _socket->read(2); - header->payloadLength = ((buf.at(0) << 8) & 0xFF00) | (buf.at(1) & 0xFF); - } - break; - - case payload_size_code_64bit: - { - QByteArray buf = _socket->read(8); - header->payloadLength = 0; - for (uint i=0; i < 8; i++) - { - header->payloadLength |= ((quint64)(buf.at(i) & 0xFF)) << (8*(7-i)); - } - } - break; - } - - // if the data is masked we need to get the key for unmasking - if (header->masked) - { - _socket->read(header->key, 4); - } -} - -/// See http://tools.ietf.org/html/rfc6455#section-5.2 for more information -void WebSocketClient::sendClose(int status, const QString& reason) -{ - Debug(_log, "Send close to %s: %d %s", QSTRING_CSTR(_socket->peerAddress().toString()), status, QSTRING_CSTR(reason)); - ErrorIf(!reason.isEmpty(), _log, "%s", QSTRING_CSTR(reason)); - _receiveBuffer.clear(); - QByteArray sendBuffer; - - sendBuffer.append(136+(status-1000)); - int length = reason.size(); - if(length >= 126) - { - sendBuffer.append( (length > 0xffff) ? 127 : 126); - int num_bytes = (length > 0xffff) ? 8 : 2; - - for(int c = num_bytes - 1; c != -1; c--) - { - sendBuffer.append( quint8((static_cast(length) >> (8 * c)) % 256)); - } - } - else - { - sendBuffer.append(quint8(length)); - } - - sendBuffer.append(reason.toUtf8()); - - _socket->write(sendBuffer); - _socket->flush(); - _socket->close(); -} - - -void WebSocketClient::handleBinaryMessage(QByteArray &data) -{ - unsigned imgSize = data.size() - 4; - unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF); - unsigned height = imgSize / width; - - if ( imgSize % width > 0 ) - { - Error(_log, "data size is not multiple of width"); - return; - } - - Image image; - image.resize(width, height); - - memcpy(image.memptr(), data.data()+4, imgSize); -} - - -qint64 WebSocketClient::sendMessage(QJsonObject obj) -{ - QJsonDocument writer(obj); - QByteArray data = writer.toJson(QJsonDocument::Compact) + "\n"; - - if (!_socket || (_socket->state() != QAbstractSocket::ConnectedState)) return 0; - - qint64 payloadWritten = 0; - quint32 payloadSize = data.size(); - const char * payload = data.data(); - - qint32 numFrames = payloadSize / FRAME_SIZE_IN_BYTES + ((quint64(payloadSize) % FRAME_SIZE_IN_BYTES) > 0 ? 1 : 0); - - for (int i = 0; i < numFrames; ++i) - { - const bool isLastFrame = (i == (numFrames - 1)); - - quint64 position = i * FRAME_SIZE_IN_BYTES; - quint32 frameSize = (payloadSize-position >= FRAME_SIZE_IN_BYTES) ? FRAME_SIZE_IN_BYTES : (payloadSize-position); - - QByteArray buf = makeFrameHeader((i == 0) ? OPCODE::TEXT : OPCODE::CONTINUATION, frameSize, isLastFrame); - sendMessage_Raw(buf); - - qint64 written = sendMessage_Raw(payload+position,frameSize); - if (written > 0) - { - payloadWritten += written; - } - else - { - _socket->flush(); - Error(_log, "Error writing bytes to socket: %s", QSTRING_CSTR(_socket->errorString())); - break; - } - } - - if (payloadSize != payloadWritten) - { - Error(_log, "Error writing bytes to socket %d bytes from %d written", payloadWritten, payloadSize); - return -1; - } - return payloadWritten; -} - -qint64 WebSocketClient::sendMessage_Raw(const char* data, quint64 size) -{ - return _socket->write(data, size); -} - -qint64 WebSocketClient::sendMessage_Raw(QByteArray &data) -{ - return _socket->write(data.data(), data.size()); -} - - -QByteArray WebSocketClient::makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame) -{ - QByteArray header; - - if (payloadLength <= 0x7FFFFFFFFFFFFFFFULL) - { - //FIN, RSV1-3, opcode (RSV-1, RSV-2 and RSV-3 are zero) - quint8 byte = static_cast((opCode & 0x0F) | (lastFrame ? 0x80 : 0x00)); - header.append(static_cast(byte)); - - byte = 0x00; - if (payloadLength <= 125) - { - byte |= static_cast(payloadLength); - header.append(static_cast(byte)); - } - else if (payloadLength <= 0xFFFFU) - { - byte |= 126; - header.append(static_cast(byte)); - quint16 swapped = qToBigEndian(static_cast(payloadLength)); - header.append(static_cast(static_cast(&swapped)), 2); - } - else - { - byte |= 127; - header.append(static_cast(byte)); - quint64 swapped = qToBigEndian(payloadLength); - header.append(static_cast(static_cast(&swapped)), 8); - } - } - else - { - Error(_log, "Payload too big!"); - } - - return header; -} diff --git a/libsrc/webserver/WebSocketClient.h b/libsrc/webserver/WebSocketClient.h deleted file mode 100644 index 520cefdb1..000000000 --- a/libsrc/webserver/WebSocketClient.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once - -#include -#include "WebSocketUtils.h" -#include - -#include - -class QTcpSocket; - -class QtHttpRequest; -class Hyperion; -class JsonAPI; - -class WebSocketClient : public QObject -{ - Q_OBJECT -public: - WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, bool localConnection, QObject* parent); - - struct WebSocketHeader - { - bool fin; - quint8 opCode; - bool masked; - quint64 payloadLength; - char key[4]; - }; - -private: - QTcpSocket* _socket; - Logger* _log; - Hyperion* _hyperion; - QScopedPointer _jsonAPI; - - void getWsFrameHeader(WebSocketHeader* header); - void sendClose(int status, const QString& reason = ""); - void handleBinaryMessage(QByteArray &data); - qint64 sendMessage_Raw(const char* data, quint64 size); - qint64 sendMessage_Raw(QByteArray &data); - QByteArray makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame); - - /// The buffer used for reading data from the socket - QByteArray _receiveBuffer; - - /// buffer for websockets multi frame receive - QByteArray _wsReceiveBuffer; - quint8 _maskKey[4]; - - bool _onContinuation = false; - - // true when data is missing for parsing - bool _notEnoughData = false; - - // websocket header store - WebSocketHeader _wsh; - - //opCode of first frame (in case of fragmented frames) - quint8 _frameOpCode; - - // masks for fields in the basic header - static uint8_t const BHB0_OPCODE = 0x0F; - static uint8_t const BHB0_RSV3 = 0x10; - static uint8_t const BHB0_RSV2 = 0x20; - static uint8_t const BHB0_RSV1 = 0x40; - static uint8_t const BHB0_FIN = 0x80; - - static uint8_t const BHB1_PAYLOAD = 0x7F; - static uint8_t const BHB1_MASK = 0x80; - - static uint8_t const payload_size_code_16bit = 0x7E; // 126 - static uint8_t const payload_size_code_64bit = 0x7F; // 127 - - static const quint64 FRAME_SIZE_IN_BYTES = 512 * 512 * 2; //maximum size of a frame when sending a message - -private slots: - void handleWebSocketFrame(); - qint64 sendMessage(QJsonObject obj); - -signals: - void handleMessage(const QString &message, const QString &httpAuthHeader); -}; diff --git a/libsrc/webserver/WebSocketJsonHandler.cpp b/libsrc/webserver/WebSocketJsonHandler.cpp new file mode 100644 index 000000000..34beeebf4 --- /dev/null +++ b/libsrc/webserver/WebSocketJsonHandler.cpp @@ -0,0 +1,63 @@ +#include "WebSocketJsonHandler.h" + +#include +#include +#include +#include + +#define NO_TRACE_SEND +#define NO_TRACE_RECEIVE + +WebSocketJsonHandler::WebSocketJsonHandler(QWebSocket* websocket, QObject* parent) + : QObject(parent) + , _websocket(websocket) + , _log(Logger::getInstance("WEBSOCKET")) +{ + connect(_websocket, &QWebSocket::textMessageReceived, this, &WebSocketJsonHandler::onTextMessageReceived); + connect(_websocket, &QWebSocket::binaryMessageReceived, this, &WebSocketJsonHandler::onBinaryMessageReceived); + connect(_websocket, &QWebSocket::disconnected, this, &WebSocketJsonHandler::onDisconnected); + + _peerAddress = _websocket->peerAddress().toString(); + _origin = websocket->origin(); + Debug(_log, "New WebSocket connection from %s initiated via: %s", QSTRING_CSTR(_peerAddress), QSTRING_CSTR(_origin)); + + bool localConnection = NetOrigin::getInstance()->isLocalAddress(_websocket->peerAddress(), _websocket->localAddress()); + + // Json processor + _jsonAPI.reset(new JsonAPI(_peerAddress, _log, localConnection, this)); + + connect(_jsonAPI.get(), &JsonAPI::callbackReady, this, &WebSocketJsonHandler::sendMessage); + connect(_jsonAPI->getCallBack().get(), &JsonCallbacks::callbackReady, this, &WebSocketJsonHandler::sendMessage); + + // Init JsonAPI + _jsonAPI->initialize(); +} + +void WebSocketJsonHandler::onTextMessageReceived(const QString& message) +{ +#ifdef RECEIVE_TRACE + qDebug() << "[" << _peerAddress << "] WebSocket message received:" << message; +#endif + _jsonAPI->handleMessage(message); +} + +void WebSocketJsonHandler::onBinaryMessageReceived(const QByteArray& message) +{ +#ifdef RECEIVE_TRACE + qDebug() << "[" << _peerAddress << "] WebSocket message received:" << message.toHex(); +#endif + Warning(_log,"Unexpected binary message received"); +} + +qint64 WebSocketJsonHandler::sendMessage(QJsonObject obj) +{ +#ifdef TRACE_SEND + qDebug() << "[" << _peerAddress << "] WebSocket send message: " << obj; +#endif + return _websocket->sendTextMessage(JsonUtils::jsonValueToQString(obj)); +} + +void WebSocketJsonHandler::onDisconnected() +{ + Debug(_log, "WebSocket disconnected from %s initiated via: %s", QSTRING_CSTR(_peerAddress), QSTRING_CSTR(_origin)); +} diff --git a/libsrc/webserver/WebSocketJsonHandler.h b/libsrc/webserver/WebSocketJsonHandler.h new file mode 100644 index 000000000..1f4c234b8 --- /dev/null +++ b/libsrc/webserver/WebSocketJsonHandler.h @@ -0,0 +1,33 @@ +#ifndef WEBSOCKETJSONHANDLER_H +#define WEBSOCKETJSONHANDLER_H + +#include +#include + +#include +#include +#include + +class WebSocketJsonHandler : public QObject +{ + Q_OBJECT + +public: + WebSocketJsonHandler(QWebSocket* websocket, QObject* parent = nullptr); + +private slots: + void onTextMessageReceived(const QString& message); + void onBinaryMessageReceived(const QByteArray& message); + void onDisconnected(); + qint64 sendMessage(QJsonObject obj); + +private: + QWebSocket* _websocket; + + Logger* _log; + QScopedPointer _jsonAPI; + QString _peerAddress; + QString _origin; +}; + +#endif // WEBSOCKETJSONHANDLER_H diff --git a/libsrc/webserver/WebSocketUtils.h b/libsrc/webserver/WebSocketUtils.h deleted file mode 100644 index f490ae972..000000000 --- a/libsrc/webserver/WebSocketUtils.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once - -/// Constants and utility functions related to WebSocket opcodes -/** - * WebSocket Opcodes are 4 bits. See RFC6455 section 5.2. - */ -namespace OPCODE -{ - enum value - { - CONTINUATION = 0x0, - TEXT = 0x1, - BINARY = 0x2, - RSV3 = 0x3, - RSV4 = 0x4, - RSV5 = 0x5, - RSV6 = 0x6, - RSV7 = 0x7, - CLOSE = 0x8, - PING = 0x9, - PONG = 0xA, - CONTROL_RSVB = 0xB, - CONTROL_RSVC = 0xC, - CONTROL_RSVD = 0xD, - CONTROL_RSVE = 0xE, - CONTROL_RSVF = 0xF - }; - - /// Check if an opcode is reserved - /** - * @param v The opcode to test. - * @return Whether or not the opcode is reserved. - */ - inline bool reserved(value v) - { - return (v >= RSV3 && v <= RSV7) || (v >= CONTROL_RSVB && v <= CONTROL_RSVF); - } - - /// Check if an opcode is invalid - /** - * Invalid opcodes are negative or require greater than 4 bits to store. - * - * @param v The opcode to test. - * @return Whether or not the opcode is invalid. - */ - inline bool invalid(value v) - { - return (v > 0xF || v < 0); - } - - /// Check if an opcode is for a control frame - /** - * @param v The opcode to test. - * @return Whether or not the opcode is a control opcode. - */ - inline bool is_control(value v) - { - return v >= 0x8; - } -} - -namespace CLOSECODE -{ - enum value - { - NORMAL = 1000, - AWAY = 1001, - TERM = 1002, - INV_TYPE = 1003, - INV_DATA = 1007, - VIOLATION = 1008, - BIG_MSG = 1009, - UNEXPECTED= 1011 - }; -}