Replace WebSocket implementation (#1819)

This commit is contained in:
LordGrey 2024-12-29 16:00:29 +01:00 committed by GitHub
parent d16142d28e
commit 0aa7df47d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 210 additions and 587 deletions

View File

@ -36,7 +36,7 @@ jobs:
if: ${{ matrix.language == 'cpp' }} if: ${{ matrix.language == 'cpp' }}
run: | run: |
sudo apt-get update 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) - 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 uses: jwlawson/actions-setup-cmake@v2

View File

@ -191,9 +191,9 @@ jobs:
- name: 📥 Install Qt - name: 📥 Install Qt
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ inputs.qt_version == '6' && '6.7' || '5.15.*' }} version: ${{ inputs.qt_version == '6' && '6.8' || '5.15.*' }}
target: 'desktop' target: 'desktop'
modules: ${{ inputs.qt_version == '6' && 'qtserialport' || '' }} modules: ${{ inputs.qt_version == '6' && 'qtserialport qtwebsockets' || '' }}
cache: 'true' cache: 'true'
cache-key-prefix: 'cache-qt-windows' cache-key-prefix: 'cache-qt-windows'

View File

@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed: Python 3.12 crashes (#1747) - Fixed: Python 3.12 crashes (#1747)
- osX Grabber: Use ScreenCaptureKit under macOS 15 and above - 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) - 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** **JSON-API**
- Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. - Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization.

View File

@ -570,7 +570,7 @@ message(STATUS "Found Qt Version: ${QT_VERSION}")
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
set(QT_MIN_VERSION "6.2.2") set(QT_MIN_VERSION "6.2.2")
else() else()
set(QT_MIN_VERSION "5.5.0") set(QT_MIN_VERSION "5.9.0")
endif() endif()
if("${QT_VERSION}" VERSION_LESS "${QT_MIN_VERSION}") if("${QT_VERSION}" VERSION_LESS "${QT_MIN_VERSION}")

View File

@ -30,16 +30,16 @@ wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/
### Ubuntu ### Ubuntu
**amd64 (Jammy):** **amd64 (Noble Numbat):**
```console ```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 ### Fedora
**amd64 (39):** **amd64 (41):**
```console ```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 ## Cross compilation on amd64 for developers
@ -61,14 +61,14 @@ cd $HYPERION_HOME
```console ```console
sudo apt-get update 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** **Ubuntu (22.04+) - Qt6 based**
```console ```console
sudo apt-get update 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** **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. The following dependencies are needed to build hyperion.ng on fedora.
```console ```console
sudo dnf -y groupinstall "Development Tools" 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..). After installing the dependencies, you can continue with the compile instructions later on this page (the more detailed way..).

View File

@ -932,13 +932,17 @@ void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonA
// Add infor about the type of setting elements // Add infor about the type of setting elements
QJsonObject settingTypes; QJsonObject settingTypes;
QJsonArray globalSettingTypes; QJsonArray globalSettingTypes;
for (const QString &type : SettingsTable().getGlobalSettingTypes()) {
SettingsTable settingsTable;
for (const QString &type : settingsTable.getGlobalSettingTypes())
{
globalSettingTypes.append(type); globalSettingTypes.append(type);
} }
settingTypes.insert("globalProperties", globalSettingTypes); settingTypes.insert("globalProperties", globalSettingTypes);
QJsonArray instanceSettingTypes; QJsonArray instanceSettingTypes;
for (const QString &type : SettingsTable().getInstanceSettingTypes()) { for (const QString &type : settingsTable.getInstanceSettingTypes())
{
instanceSettingTypes.append(type); instanceSettingTypes.append(type);
} }
settingTypes.insert("instanceProperties", instanceSettingTypes); settingTypes.insert("instanceProperties", instanceSettingTypes);

View File

@ -67,12 +67,12 @@ int ProviderFtdi::open()
Debug(_log, "Opening FTDI device=%s", QSTRING_CSTR(_deviceName)); 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 */ /* 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_disable_bitbang(_ftdic)) < 0)
FTDI_CHECK_RESULT((rc = ftdi_setflowctrl(_ftdic, SIO_DISABLE_FLOW_CTRL)) < 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, 0x00, BITMODE_RESET)) < 0)
FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0xff, BITMODE_MPSSE)) < 0); FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0xff, BITMODE_MPSSE)) < 0)
double reference_clock = 60e6; double reference_clock = 60e6;
int divisor = (reference_clock / 2 / _baudRate_Hz) - 1; int divisor = (reference_clock / 2 / _baudRate_Hz) - 1;
@ -86,7 +86,7 @@ int ProviderFtdi::open()
pinDirection pinDirection
}; };
FTDI_CHECK_RESULT((rc = ftdi_write_data(_ftdic, buf.data(), buf.size())) != buf.size()); FTDI_CHECK_RESULT(static_cast<size_t>(rc = ftdi_write_data(_ftdic, buf.data(), static_cast<int>(buf.size()))) != buf.size())
_isDeviceReady = true; _isDeviceReady = true;
return rc; 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 // 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]); 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<size_t>(rc = ftdi_write_data(_ftdic, buf.data(), static_cast<int>(buf.size()))) != buf.size())
return rc; return rc;
} }
@ -152,7 +152,7 @@ QJsonObject ProviderFtdi::discover(const QJsonObject & /*params*/)
struct ftdi_device_list *curdev = devlist; struct ftdi_device_list *curdev = devlist;
QMap<QString, uint8_t> deviceIndexes; QMap<QString, uint8_t> deviceIndexes;
while (curdev) while (curdev != nullptr)
{ {
libusb_device_descriptor desc; libusb_device_descriptor desc;
int rc = libusb_get_device_descriptor(curdev->dev, &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 vendorIdentifier = QString("0x%1").arg(desc.idVendor, 4, 16, QChar{'0'});
QString productIdentifier = QString("0x%1").arg(desc.idProduct, 4, 16, QChar{'0'}); QString productIdentifier = QString("0x%1").arg(desc.idProduct, 4, 16, QChar{'0'});
QString vendorAndProduct = QString("%1:%2") QString vendorAndProduct = QString("%1:%2")
.arg(vendorIdentifier) .arg(vendorIdentifier,productIdentifier);
.arg(productIdentifier);
uint8_t deviceIndex = deviceIndexes.value(vendorAndProduct, 0); uint8_t deviceIndex = deviceIndexes.value(vendorAndProduct, 0);
char serial_string[128] = {0}; char serial_string[128] = {0};
@ -174,7 +173,7 @@ QJsonObject ProviderFtdi::discover(const QJsonObject & /*params*/)
QString ftdiOpenString; QString ftdiOpenString;
if(!serialNumber.isEmpty()) if(!serialNumber.isEmpty())
{ {
ftdiOpenString = QString("s:%1:%2").arg(vendorAndProduct).arg(serialNumber); ftdiOpenString = QString("s:%1:%2").arg(vendorAndProduct, serialNumber);
} }
else else
{ {

View File

@ -25,7 +25,7 @@ PythonProgram::PythonProgram(const QString& name, Logger* log) :
PyEval_RestoreThread(mainThreadState); PyEval_RestoreThread(mainThreadState);
_tstate = Py_NewInterpreter(); _tstate = Py_NewInterpreter();
#else #else
PyThreadState* prev = PyThreadState_Swap(NULL); PyThreadState_Swap(NULL);
// Create a new interpreter configuration object // Create a new interpreter configuration object
PyInterpreterConfig config{}; PyInterpreterConfig config{};

View File

@ -50,16 +50,15 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i
outputImage.resize(outputWidth, outputHeight); outputImage.resize(outputWidth, outputHeight);
int xDestStart, xDestEnd; int xDestStart {0};
int yDestStart, yDestEnd; int xDestEnd = {outputWidth-1};
int yDestStart = {0};
int yDestEnd = {outputWidth-1};
switch (_flipMode) switch (_flipMode)
{ {
case FlipMode::NO_CHANGE: case FlipMode::NO_CHANGE:
xDestStart = 0; //use the initalized values
xDestEnd = outputWidth-1;
yDestStart = 0;
yDestEnd = outputHeight-1;
break; break;
case FlipMode::HORIZONTAL: case FlipMode::HORIZONTAL:
xDestStart = 0; xDestStart = 0;

View File

@ -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(GLOB_RECURSE webFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig/*)
file(RELATIVE_PATH webConfigPath ${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/StaticFileServing.cpp
${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.h ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.h
${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.cpp ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.cpp
${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketClient.h ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketJsonHandler.h
${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketClient.cpp ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketJsonHandler.cpp
${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketUtils.h
${CMAKE_BINARY_DIR}/WebConfig.qrc ${CMAKE_BINARY_DIR}/WebConfig.qrc
) )
target_link_libraries(webserver target_link_libraries(webserver
Qt${QT_VERSION_MAJOR}::WebSockets
hyperion hyperion
hyperion-utils hyperion-utils
hyperion-api hyperion-api

View File

@ -5,7 +5,7 @@
#include "QtHttpReply.h" #include "QtHttpReply.h"
#include "QtHttpServer.h" #include "QtHttpServer.h"
#include "QtHttpHeader.h" #include "QtHttpHeader.h"
#include "WebSocketClient.h" #include "WebSocketJsonHandler.h"
#include "WebJsonRpc.h" #include "WebJsonRpc.h"
#include <QCryptographicHash> #include <QCryptographicHash>
@ -27,8 +27,9 @@ QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, const bool& localCo
, m_localConnection(localConnection) , m_localConnection(localConnection)
, m_websocketClient(nullptr) , m_websocketClient(nullptr)
, m_webJsonRpc (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) QString QtHttpClientWrapper::getGuid (void)
@ -50,6 +51,11 @@ void QtHttpClientWrapper::onClientDataReceived (void)
{ {
if (m_sockClient != Q_NULLPTR) if (m_sockClient != Q_NULLPTR)
{ {
if (!m_sockClient->isTransactionStarted())
{
m_sockClient->startTransaction();
}
while (m_sockClient->bytesAvailable () != 0) while (m_sockClient->bytesAvailable () != 0)
{ {
QByteArray line = m_sockClient->readLine (); QByteArray line = m_sockClient->readLine ();
@ -162,22 +168,30 @@ void QtHttpClientWrapper::onClientDataReceived (void)
{ {
case RequestParsed: // a valid request has ben fully parsed case RequestParsed: // a valid request has ben fully parsed
{ {
// Catch websocket header "Upgrade" const auto& upgradeValue = m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower();
if(m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower() == "websocket") if (upgradeValue == "websocket")
{ {
if(m_websocketClient == Q_NULLPTR) if(m_websocketClient == Q_NULLPTR)
{ {
// disconnect this slot from socket for further requests // disconnect this slot from socket for further requests
disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived);
// disabling packet bunching m_sockClient->rollbackTransaction();
m_sockClient->setSocketOption(QAbstractSocket::LowDelayOption, 1);
m_sockClient->setSocketOption(QAbstractSocket::KeepAliveOption, 1); QString servername = QCoreApplication::applicationName() + QLatin1Char('/') + HYPERION_VERSION;
m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, m_localConnection, this); 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; break;
} }
m_sockClient->commitTransaction();
// add post data to request and catch /jsonrpc subroute url // add post data to request and catch /jsonrpc subroute url
if ( m_currentRequest->getCommand() == "POST") if ( m_currentRequest->getCommand() == "POST")
{ {
@ -227,6 +241,8 @@ void QtHttpClientWrapper::onClientDataReceived (void)
case ParsingError: // there was an error durin one of parsing steps case ParsingError: // there was an error durin one of parsing steps
{ {
m_sockClient->readAll (); // clear remaining buffer to ignore content m_sockClient->readAll (); // clear remaining buffer to ignore content
m_sockClient->commitTransaction();
QtHttpReply reply (m_serverHandle); QtHttpReply reply (m_serverHandle);
reply.setStatusCode (QtHttpReply::BadRequest); reply.setStatusCode (QtHttpReply::BadRequest);
reply.appendRawData (QByteArrayLiteral ("<h1>Bad Request (HTTP parsing error) !</h1>")); reply.appendRawData (QByteArrayLiteral ("<h1>Bad Request (HTTP parsing error) !</h1>"));
@ -365,3 +381,17 @@ void QtHttpClientWrapper::closeConnection()
} }
m_sockClient->close (); 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!";
}
}

View File

@ -3,6 +3,9 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QWebSocketServer>
#include <QCoreApplication>
#include <QScopedPointer>
class QTcpSocket; class QTcpSocket;
@ -39,8 +42,12 @@ public:
/// ///
void closeConnection(); void closeConnection();
signals:
void newWebSocketConnection();
private slots: private slots:
void onClientDataReceived (void); void onClientDataReceived (void);
void onNewWebSocketConnection();
protected: protected:
ParsingStatus sendReplyToClient (QtHttpReply * reply); ParsingStatus sendReplyToClient (QtHttpReply * reply);
@ -59,6 +66,7 @@ private:
WebSocketClient * m_websocketClient; WebSocketClient * m_websocketClient;
WebJsonRpc * m_webJsonRpc; WebJsonRpc * m_webJsonRpc;
QByteArray m_fragment; QByteArray m_fragment;
QScopedPointer<QWebSocketServer> m_websocketServer;
}; };
#endif // QTHTTPCLIENTWRAPPER_H #endif // QTHTTPCLIENTWRAPPER_H

View File

@ -40,7 +40,7 @@ QtHttpServer::QtHttpServer (QObject * parent)
, m_netOrigin (NetOrigin::getInstance()) , m_netOrigin (NetOrigin::getInstance())
{ {
m_sockServer = new QtHttpServerWrapper (this); 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) void QtHttpServer::start (quint16 port)
@ -81,27 +81,27 @@ void QtHttpServer::onClientConnected (void)
{ {
if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) 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 (m_useSsl)
{ {
if (QSslSocket * ssl = qobject_cast<QSslSocket *> (sock)) if (QSslSocket* ssl = qobject_cast<QSslSocket*> (sock))
{ {
connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); connect(ssl, SslErrorSignal(&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors);
connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); connect(ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted);
connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); connect(ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError);
connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); connect(ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged);
ssl->setLocalCertificateChain (m_sslCerts); ssl->setLocalCertificateChain(m_sslCerts);
ssl->setPrivateKey (m_sslKey); ssl->setPrivateKey(m_sslKey);
ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); ssl->setPeerVerifyMode(QSslSocket::AutoVerifyPeer);
ssl->startServerEncryption (); ssl->startServerEncryption();
} }
} }
QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, m_netOrigin->isLocalAddress(sock->peerAddress(), sock->localAddress()), this); QtHttpClientWrapper* wrapper = new QtHttpClientWrapper(sock, m_netOrigin->isLocalAddress(sock->peerAddress(), sock->localAddress()), this);
m_socksClientsHash.insert (sock, wrapper); m_socksClientsHash.insert(sock, wrapper);
emit clientConnected (wrapper->getGuid ()); emit clientConnected (wrapper->getGuid ());
} }
else else

View File

@ -46,21 +46,22 @@ public:
typedef void (QSslSocket::* SslErrorSignal) (const QList<QSslError> &); typedef void (QSslSocket::* SslErrorSignal) (const QList<QSslError> &);
const QString & getServerName (void) const { return m_serverName; }; const QString & getServerName (void) const { return m_serverName; }
quint16 getServerPort (void) const { return m_sockServer->serverPort(); }; quint16 getServerPort (void) const { return m_sockServer->serverPort(); }
QString getErrorString (void) const { return m_sockServer->errorString(); }; QString getErrorString (void) const { return m_sockServer->errorString(); }
bool isListening() { return m_sockServer->isListening(); }; bool isListening() { return m_sockServer->isListening(); }
bool isSecure() { return m_useSsl; }
public slots: public slots:
void start (quint16 port = 0); void start (quint16 port = 0);
void stop (void); void stop (void);
void setUseSecure (const bool ssl = true); void setUseSecure (bool ssl = true);
void setServerName (const QString & serverName) { m_serverName = serverName; }; void setServerName (const QString & serverName) { m_serverName = serverName; }
void setPrivateKey (const QSslKey & key) { m_sslKey = key; }; void setPrivateKey (const QSslKey & key) { m_sslKey = key; }
void setCertificates (const QList<QSslCertificate> & certs) { m_sslCerts = certs; }; void setCertificates (const QList<QSslCertificate> & certs) { m_sslCerts = certs; }
QSslKey getPrivateKey() { return m_sslKey; }; QSslKey getPrivateKey() { return m_sslKey; }
QList<QSslCertificate> getCertificates() { return m_sslCerts; }; QList<QSslCertificate> getCertificates() { return m_sslCerts; }
signals: signals:
void started (quint16 port); void started (quint16 port);
@ -73,10 +74,10 @@ signals:
private slots: private slots:
void onClientConnected (void); void onClientConnected (void);
void onClientDisconnected (void); void onClientDisconnected (void);
void onClientSslEncrypted (void) { }; void onClientSslEncrypted (void) { }
void onClientSslPeerVerifyError (const QSslError & err) { Q_UNUSED (err) }; void onClientSslPeerVerifyError (const QSslError & err) { Q_UNUSED (err) }
void onClientSslErrors (const QList<QSslError> & errors) { Q_UNUSED (errors) }; void onClientSslErrors (const QList<QSslError> & errors) { Q_UNUSED (errors) }
void onClientSslModeChanged (QSslSocket::SslMode mode) { Q_UNUSED (mode) }; void onClientSslModeChanged (QSslSocket::SslMode mode) { Q_UNUSED (mode) }
private: private:
bool m_useSsl; bool m_useSsl;

View File

@ -1,359 +0,0 @@
#include "WebSocketClient.h"
#include "QtHttpRequest.h"
#include "QtHttpHeader.h"
#include <hyperion/Hyperion.h>
#include <api/JsonAPI.h>
#include <api/JsonCallbacks.h>
#include <QTcpSocket>
#include <QtEndian>
#include <QCryptographicHash>
#include <QJsonObject>
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<unsigned long long>(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<ColorRgb> 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<quint8>((opCode & 0x0F) | (lastFrame ? 0x80 : 0x00));
header.append(static_cast<char>(byte));
byte = 0x00;
if (payloadLength <= 125)
{
byte |= static_cast<quint8>(payloadLength);
header.append(static_cast<char>(byte));
}
else if (payloadLength <= 0xFFFFU)
{
byte |= 126;
header.append(static_cast<char>(byte));
quint16 swapped = qToBigEndian<quint16>(static_cast<quint16>(payloadLength));
header.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 2);
}
else
{
byte |= 127;
header.append(static_cast<char>(byte));
quint64 swapped = qToBigEndian<quint64>(payloadLength);
header.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 8);
}
}
else
{
Error(_log, "Payload too big!");
}
return header;
}

View File

@ -1,82 +0,0 @@
#pragma once
#include <utils/Logger.h>
#include "WebSocketUtils.h"
#include <api/JsonAPI.h>
#include <QScopedPointer>
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> _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);
};

View File

@ -0,0 +1,63 @@
#include "WebSocketJsonHandler.h"
#include <api/JsonAPI.h>
#include <api/JsonCallbacks.h>
#include <utils/JsonUtils.h>
#include <utils/NetOrigin.h>
#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));
}

View File

@ -0,0 +1,33 @@
#ifndef WEBSOCKETJSONHANDLER_H
#define WEBSOCKETJSONHANDLER_H
#include <utils/Logger.h>
#include <api/JsonAPI.h>
#include <QObject>
#include <QWebSocket>
#include <QScopedPointer>
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> _jsonAPI;
QString _peerAddress;
QString _origin;
};
#endif // WEBSOCKETJSONHANDLER_H

View File

@ -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
};
}