From 0f9f3a17e70ad3faf753656858d9249484573513 Mon Sep 17 00:00:00 2001 From: brindosch Date: Mon, 20 Nov 2017 00:06:45 +0100 Subject: [PATCH] Move WebSocket to Webserver & HttpJsonRpc async (#486) * Move WebSocket to Webserver and HttpJsonRpc is now async * revert... --- assets/webconfig/js/hyperion.js | 116 +++--- include/jsonserver/JsonServer.h | 6 +- include/utils/JsonProcessor.h | 7 +- libsrc/jsonserver/JsonClientConnection.cpp | 387 +-------------------- libsrc/jsonserver/JsonClientConnection.h | 186 +--------- libsrc/jsonserver/JsonServer.cpp | 20 +- libsrc/utils/JsonProcessor.cpp | 16 +- libsrc/webconfig/QtHttpClientWrapper.cpp | 40 ++- libsrc/webconfig/QtHttpClientWrapper.h | 16 +- libsrc/webconfig/QtHttpHeader.cpp | 58 +-- libsrc/webconfig/QtHttpHeader.h | 5 + libsrc/webconfig/QtHttpServer.cpp | 1 - libsrc/webconfig/StaticFileServing.cpp | 53 +-- libsrc/webconfig/StaticFileServing.h | 3 - libsrc/webconfig/WebJsonRpc.cpp | 38 ++ libsrc/webconfig/WebJsonRpc.h | 27 ++ libsrc/webconfig/WebSocketClient.cpp | 336 ++++++++++++++++++ libsrc/webconfig/WebSocketClient.h | 72 ++++ libsrc/webconfig/WebSocketUtils.h | 68 ++++ 19 files changed, 733 insertions(+), 722 deletions(-) create mode 100644 libsrc/webconfig/WebJsonRpc.cpp create mode 100644 libsrc/webconfig/WebJsonRpc.h create mode 100644 libsrc/webconfig/WebSocketClient.cpp create mode 100644 libsrc/webconfig/WebSocketClient.h create mode 100644 libsrc/webconfig/WebSocketUtils.h diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index bc6ea175..a3149490 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -65,73 +65,71 @@ function initWebSocket() { if (websocket == null) { - $.ajax({ url: "/cgi/cfg_jsonserver" }).done(function(data) { - jsonPort = data.substr(1); - websocket = new WebSocket('ws://'+document.location.hostname+data); + jsonPort = (document.location.port == '') ? '80' : document.location.port; + websocket = new WebSocket('ws://'+document.location.hostname+":"+document.location.port); + console.log(jsonPort) + websocket.onopen = function (event) { + $(hyperion).trigger({type:"open"}); - websocket.onopen = function (event) { - $(hyperion).trigger({type:"open"}); + $(hyperion).on("cmd-serverinfo", function(event) { + watchdog = 0; + }); + }; - $(hyperion).on("cmd-serverinfo", function(event) { - watchdog = 0; - }); - }; + websocket.onclose = function (event) { + // See http://tools.ietf.org/html/rfc6455#section-7.4.1 + var reason; + switch(event.code) + { + case 1000: reason = "Normal closure, meaning that the purpose for which the connection was established has been fulfilled."; break; + case 1001: reason = "An endpoint is \"going away\", such as a server going down or a browser having navigated away from a page."; break; + case 1002: reason = "An endpoint is terminating the connection due to a protocol error"; break; + case 1003: reason = "An endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it receives a binary message)."; break; + case 1004: reason = "Reserved. The specific meaning might be defined in the future."; break; + case 1005: reason = "No status code was actually present."; break; + case 1006: reason = "The connection was closed abnormally, e.g., without sending or receiving a Close control frame"; break; + case 1007: reason = "An endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629] data within a text message)."; break; + case 1008: reason = "An endpoint is terminating the connection because it has received a message that \"violates its policy\". This reason is given either if there is no other sutible reason, or if there is a need to hide specific details about the policy."; break; + case 1009: reason = "An endpoint is terminating the connection because it has received a message that is too big for it to process."; break; + case 1010: reason = "An endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didn't return them in the response message of the WebSocket handshake.
Specifically, the extensions that are needed are: " + event.reason; break; + case 1011: reason = "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request."; break; + case 1015: reason = "The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified)."; break; + default: reason = "Unknown reason"; + } + console.log("[websocket::onclose] "+reason) + $(hyperion).trigger({type:"close", reason:reason}); + watchdog = 10; + connectionLostDetection(); + }; - websocket.onclose = function (event) { - // See http://tools.ietf.org/html/rfc6455#section-7.4.1 - var reason; - switch(event.code) + websocket.onmessage = function (event) { + try + { + response = JSON.parse(event.data); + success = response.success; + cmd = response.command; + if (success) { - case 1000: reason = "Normal closure, meaning that the purpose for which the connection was established has been fulfilled."; break; - case 1001: reason = "An endpoint is \"going away\", such as a server going down or a browser having navigated away from a page."; break; - case 1002: reason = "An endpoint is terminating the connection due to a protocol error"; break; - case 1003: reason = "An endpoint is terminating the connection because it has received a type of data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it receives a binary message)."; break; - case 1004: reason = "Reserved. The specific meaning might be defined in the future."; break; - case 1005: reason = "No status code was actually present."; break; - case 1006: reason = "The connection was closed abnormally, e.g., without sending or receiving a Close control frame"; break; - case 1007: reason = "An endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629] data within a text message)."; break; - case 1008: reason = "An endpoint is terminating the connection because it has received a message that \"violates its policy\". This reason is given either if there is no other sutible reason, or if there is a need to hide specific details about the policy."; break; - case 1009: reason = "An endpoint is terminating the connection because it has received a message that is too big for it to process."; break; - case 1010: reason = "An endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didn't return them in the response message of the WebSocket handshake.
Specifically, the extensions that are needed are: " + event.reason; break; - case 1011: reason = "A server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request."; break; - case 1015: reason = "The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified)."; break; - default: reason = "Unknown reason"; + $(hyperion).trigger({type:"cmd-"+cmd, response:response}); } - console.log("[websocket::onclose] "+reason) - $(hyperion).trigger({type:"close", reason:reason}); - watchdog = 10; - connectionLostDetection(); - }; - - websocket.onmessage = function (event) { - try + else { - response = JSON.parse(event.data); - success = response.success; - cmd = response.command; - if (success) - { - $(hyperion).trigger({type:"cmd-"+cmd, response:response}); - } - else - { - error = response.hasOwnProperty("error")? response.error : "unknown"; - $(hyperion).trigger({type:"error",reason:error}); - console.log("[websocket::onmessage] "+error) - } + error = response.hasOwnProperty("error")? response.error : "unknown"; + $(hyperion).trigger({type:"error",reason:error}); + console.log("[websocket::onmessage] "+error) } - catch(exception_error) - { - $(hyperion).trigger({type:"error",reason:exception_error}); - console.log("[websocket::onmessage] "+exception_error) - } - }; + } + catch(exception_error) + { + $(hyperion).trigger({type:"error",reason:exception_error}); + console.log("[websocket::onmessage] "+exception_error) + } + }; - websocket.onerror = function (error) { - $(hyperion).trigger({type:"error",reason:error}); - console.log("[websocket::onerror] "+error) - }; - }); + websocket.onerror = function (error) { + $(hyperion).trigger({type:"error",reason:error}); + console.log("[websocket::onerror] "+error) + }; } } else diff --git a/include/jsonserver/JsonServer.h b/include/jsonserver/JsonServer.h index 935a8f05..912e14ac 100644 --- a/include/jsonserver/JsonServer.h +++ b/include/jsonserver/JsonServer.h @@ -1,8 +1,5 @@ #pragma once -// system includes -#include - // Qt includes #include #include @@ -45,9 +42,8 @@ private slots: /// /// Slot which is called when a client closes a connection - /// @param connection The Connection object which is being closed /// - void closedConnection(JsonClientConnection * connection); + void closedConnection(void); /// forward message to all json slaves void forwardJsonMessage(const QJsonObject &message); diff --git a/include/utils/JsonProcessor.h b/include/utils/JsonProcessor.h index 1b7d7e7e..5ccd8e81 100644 --- a/include/utils/JsonProcessor.h +++ b/include/utils/JsonProcessor.h @@ -46,18 +46,17 @@ public: /// /// @param peerAddress provide the Address of the peer /// @param log The Logger class of the creator + /// @param parent Parent QObject /// @param noListener if true, this instance won't listen for hyperion push events /// - JsonProcessor(QString peerAddress, Logger* log, bool noListener = false); - ~JsonProcessor(); + JsonProcessor(QString peerAddress, Logger* log, QObject* parent, bool noListener = false); /// /// Handle an incoming JSON message /// /// @param message the incoming message as string - /// @param peerAddress overwrite peerAddress of constructor /// - void handleMessage(const QString & message, const QString peerAddress = NULL); + void handleMessage(const QString & message); /// /// send a forced serverinfo to a client diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index c1a7b273..554361b2 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -1,69 +1,23 @@ -// Qt includes -#include -#include -#include - // project includes #include "JsonClientConnection.h" - -const quint64 FRAME_SIZE_IN_BYTES = 512 * 512 * 2; //maximum size of a frame when sending a message +#include +#include JsonClientConnection::JsonClientConnection(QTcpSocket *socket) : QObject() , _socket(socket) - , _hyperion(Hyperion::getInstance()) , _receiveBuffer() - , _webSocketHandshakeDone(false) - , _onContinuation(false) , _log(Logger::getInstance("JSONCLIENTCONNECTION")) - , _notEnoughData(false) - , _clientAddress(socket->peerAddress()) - , _connectionMode(CON_MODE::INIT) { - // connect internal signals and slots - connect(_socket, SIGNAL(disconnected()), this, SLOT(socketClosed())); - connect(_socket, SIGNAL(readyRead()), this, SLOT(readData())); - + connect(_socket, &QTcpSocket::disconnected, this, &JsonClientConnection::disconnected); + connect(_socket, &QTcpSocket::readyRead, this, &JsonClientConnection::readRequest); // create a new instance of JsonProcessor - _jsonProcessor = new JsonProcessor(_clientAddress.toString(), _log); + _jsonProcessor = new JsonProcessor(socket->peerAddress().toString(), _log, this); // get the callback messages from JsonProcessor and send it to the client connect(_jsonProcessor,SIGNAL(callbackMessage(QJsonObject)),this,SLOT(sendMessage(QJsonObject))); } -JsonClientConnection::~JsonClientConnection() -{ - delete _socket; - delete _jsonProcessor; -} - -void JsonClientConnection::readData() -{ - switch(_connectionMode) - { - case CON_MODE::INIT: - _receiveBuffer = _socket->readAll(); // initial read to determine connection - _connectionMode = (_receiveBuffer.contains("Upgrade: websocket")) ? CON_MODE::WEBSOCKET : CON_MODE::RAW; - - // init websockets - if (_connectionMode == CON_MODE::WEBSOCKET) - { - doWebSocketHandshake(); - break; - } - // if no ws, hand over the data to raw handling - - case CON_MODE::RAW: - handleRawJsonData(); - break; - - case CON_MODE::WEBSOCKET: - handleWebSocketFrame(); - break; - } -} - - -void JsonClientConnection::handleRawJsonData() +void JsonClientConnection::readRequest() { _receiveBuffer += _socket->readAll(); // raw socket data, handling as usual @@ -84,339 +38,16 @@ void JsonClientConnection::handleRawJsonData() } } -void JsonClientConnection::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); - } -} - - -void JsonClientConnection::handleWebSocketFrame() -{ - //printf("frame\n"); - - // we are on no continious reading from socket from call before - if (!_notEnoughData) - { - getWsFrameHeader(&_wsh); - } - - if(_socket->bytesAvailable() < (qint64)_wsh.payloadLength) - { - //printf("not enough data %llu %llu\n", _socket->bytesAvailable(), _wsh.payloadLength); - _notEnoughData=true; - return; - } - _notEnoughData = false; - - QByteArray buf = _socket->read(_wsh.payloadLength); - //printf("opcode %x payload bytes %llu avail: %llu\n", _wsh.opCode, _wsh.payloadLength, _socket->bytesAvailable()); - - 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: - { - // check for protocal 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 (_wsh.opCode == OPCODE::TEXT) - { - _jsonProcessor->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 recievied, protocol violation!"); - } - - default: - Warning(_log, "strange %d\n%s\n", _wsh.opCode, QSTRING_CSTR(QString(buf))); - } -} - -/// See http://tools.ietf.org/html/rfc6455#section-5.2 for more information -void JsonClientConnection::sendClose(int status, QString reason) -{ - Info(_log, "send close: %d %s", status, QSTRING_CSTR(reason)); - ErrorIf(!reason.isEmpty(), _log, 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); - - _socket->write(sendBuffer); - _socket->flush(); - _socket->close(); -} - -void JsonClientConnection::doWebSocketHandshake() -{ - // http header, might not be a very reliable check... - Debug(_log, "Websocket handshake"); - - // get the key to prepare an answer - int start = _receiveBuffer.indexOf("Sec-WebSocket-Key") + 19; - QByteArray value = _receiveBuffer.mid(start, _receiveBuffer.indexOf("\r\n", start) - start); - _receiveBuffer.clear(); - - // must be always appended - value += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - - // generate sha1 hash - QByteArray hash = QCryptographicHash::hash(value, QCryptographicHash::Sha1).toBase64(); - - // prepare an answer - 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(); - - // we are in WebSocket mode, data frames should follow next - _webSocketHandshakeDone = true; -} - -void JsonClientConnection::socketClosed() -{ - _webSocketHandshakeDone = false; - emit connectionClosed(this); -} - -QByteArray JsonClientConnection::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, "JsonClientConnection::getHeader: payload too big!"); - } - - return header; -} - qint64 JsonClientConnection::sendMessage(QJsonObject message) { QJsonDocument writer(message); - QByteArray serializedReply = writer.toJson(QJsonDocument::Compact) + "\n"; + QByteArray data = writer.toJson(QJsonDocument::Compact) + "\n"; if (!_socket || (_socket->state() != QAbstractSocket::ConnectedState)) return 0; - if (_webSocketHandshakeDone) return sendMessage_Websockets(serializedReply); - - return sendMessage_Raw(serializedReply); -} - -qint64 JsonClientConnection::sendMessage_Raw(const char* data, quint64 size) -{ - return _socket->write(data, size); -} - -qint64 JsonClientConnection::sendMessage_Raw(QByteArray &data) -{ return _socket->write(data.data(), data.size()); } -qint64 JsonClientConnection::sendMessage_Websockets(QByteArray &data) +void JsonClientConnection::disconnected(void) { - 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(OPCODE::TEXT, 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 writte", payloadWritten, payloadSize); - return -1; - } - return payloadWritten; -} - -void JsonClientConnection::handleBinaryMessage(QByteArray &data) -{ - uint8_t priority = data.at(0); - unsigned duration_s = data.at(1); - unsigned imgSize = data.size() - 4; - unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF); - unsigned height = imgSize / width; - - if ( ! (imgSize) % width) - { - Error(_log, "data size is not multiple of width"); - return; - } - - Image image; - image.resize(width, height); - - memcpy(image.memptr(), data.data()+4, imgSize); - _hyperion->setImage(priority, image, duration_s*1000); + emit connectionClosed(); } diff --git a/libsrc/jsonserver/JsonClientConnection.h b/libsrc/jsonserver/JsonClientConnection.h index e8c7d1c3..1195e7c5 100644 --- a/libsrc/jsonserver/JsonClientConnection.h +++ b/libsrc/jsonserver/JsonClientConnection.h @@ -1,95 +1,14 @@ #pragma once -#include - // Qt includes -#include -#include -#include #include - -// Hyperion includes -#include +#include // util includes #include -#include - -/// 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 - }; -} - -namespace CON_MODE { - enum value { - INIT = 0, - RAW = 1, - WEBSOCKET = 2 - }; -} +class JsonProcessor; +class QTcpSocket; /// /// The Connection object created by \a JsonServer when a new connection is establshed @@ -105,117 +24,28 @@ public: /// JsonClientConnection(QTcpSocket * socket); - /// - /// Destructor - /// - ~JsonClientConnection(); +signals: + void connectionClosed(); - struct WebSocketHeader - { - bool fin; - quint8 opCode; - bool masked; - quint64 payloadLength; - char key[4]; - }; public slots: qint64 sendMessage(QJsonObject); -signals: - /// - /// Signal which is emitted when the connection is being closed - /// @param connection This connection object - /// - void connectionClosed(JsonClientConnection * connection); - private slots: /// /// Slot called when new data has arrived /// - void readData(); - - /// - /// Slot called when this connection is being closed - /// - void socketClosed(); + void readRequest(); + void disconnected(); private: + QTcpSocket* _socket; /// new instance of JsonProcessor JsonProcessor * _jsonProcessor; - /// - /// Do handshake for a websocket connection - /// - void doWebSocketHandshake(); - - /// - /// Handle incoming websocket data frame - /// - void handleWebSocketFrame(); - - /// - /// Handle incoming raw data frame - /// - void handleRawJsonData(); - - /// - /// create ws header from socket and decode it - /// - QByteArray makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame); - - /// - /// handle binary message - /// - /// This function should be placed elsewhere .... - /// - void handleBinaryMessage(QByteArray &data); - - qint64 sendMessage_Raw(const char* data, quint64 size); - qint64 sendMessage_Raw(QByteArray &data); - qint64 sendMessage_Websockets(QByteArray &data); - void sendClose(int status, QString reason = ""); - - void getWsFrameHeader(WebSocketHeader* header); - - /// The TCP-Socket that is connected tot the Json-client - QTcpSocket * _socket; - - /// Link to Hyperion for writing led-values to a priority channel - Hyperion * _hyperion; - /// The buffer used for reading data from the socket QByteArray _receiveBuffer; - /// buffer for websockets multi frame receive - QByteArray _wsReceiveBuffer; - quint8 _maskKey[4]; - - /// used for WebSocket detection and connection handling - bool _webSocketHandshakeDone; - - bool _onContinuation; - /// The logger instance Logger * _log; - - WebSocketHeader _wsh; - bool _notEnoughData; - - /// address of client - QHostAddress _clientAddress; - - CON_MODE::value _connectionMode; - // 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 }; diff --git a/libsrc/jsonserver/JsonServer.cpp b/libsrc/jsonserver/JsonServer.cpp index 7b38826c..3c8e9e73 100644 --- a/libsrc/jsonserver/JsonServer.cpp +++ b/libsrc/jsonserver/JsonServer.cpp @@ -57,21 +57,23 @@ uint16_t JsonServer::getPort() const void JsonServer::newConnection() { - QTcpSocket * socket = _server.nextPendingConnection(); - - if (socket != nullptr) + while(_server.hasPendingConnections()) { - Debug(_log, "New connection from: %s ",socket->localAddress().toString().toStdString().c_str()); - JsonClientConnection * connection = new JsonClientConnection(socket); - _openConnections.insert(connection); + if (QTcpSocket * socket = _server.nextPendingConnection()) + { + Debug(_log, "New connection from: %s ",socket->localAddress().toString().toStdString().c_str()); + JsonClientConnection * connection = new JsonClientConnection(socket); + _openConnections.insert(connection); - // register slot for cleaning up after the connection closed - connect(connection, SIGNAL(connectionClosed(JsonClientConnection*)), this, SLOT(closedConnection(JsonClientConnection*))); + // register slot for cleaning up after the connection closed + connect(connection, &JsonClientConnection::connectionClosed, this, &JsonServer::closedConnection); + } } } -void JsonServer::closedConnection(JsonClientConnection *connection) +void JsonServer::closedConnection(void) { + JsonClientConnection* connection = qobject_cast(sender()); Debug(_log, "Connection closed"); _openConnections.remove(connection); diff --git a/libsrc/utils/JsonProcessor.cpp b/libsrc/utils/JsonProcessor.cpp index e35ff14f..b1d6dc64 100644 --- a/libsrc/utils/JsonProcessor.cpp +++ b/libsrc/utils/JsonProcessor.cpp @@ -35,8 +35,8 @@ using namespace hyperion; std::map JsonProcessor::_componentsPrevState; -JsonProcessor::JsonProcessor(QString peerAddress, Logger* log, bool noListener) - : QObject() +JsonProcessor::JsonProcessor(QString peerAddress, Logger* log, QObject* parent, bool noListener) + : QObject(parent) , _peerAddress(peerAddress) , _log(log) , _hyperion(Hyperion::getInstance()) @@ -61,16 +61,8 @@ JsonProcessor::JsonProcessor(QString peerAddress, Logger* log, bool noListener) _image_stream_mutex.unlock(); } -JsonProcessor::~JsonProcessor() +void JsonProcessor::handleMessage(const QString& messageString) { - -} - -void JsonProcessor::handleMessage(const QString& messageString, const QString peerAddress) -{ - if(!peerAddress.isNull()) - _peerAddress = peerAddress; - const QString ident = "JsonRpc@"+_peerAddress; Q_INIT_RESOURCE(JSONRPC_schemas); QJsonObject message; @@ -808,7 +800,7 @@ void JsonProcessor::handleConfigSetCommand(const QJsonObject& message, const QSt else if (!validate.first && validate.second) { Warning(_log,"Errors have been found in the configuration file. Automatic correction is applied"); - + QStringList schemaErrors = schemaChecker.getMessages(); for (auto & schemaError : schemaErrors) Info(_log, QSTRING_CSTR(schemaError)); diff --git a/libsrc/webconfig/QtHttpClientWrapper.cpp b/libsrc/webconfig/QtHttpClientWrapper.cpp index 4900a935..0720abe5 100644 --- a/libsrc/webconfig/QtHttpClientWrapper.cpp +++ b/libsrc/webconfig/QtHttpClientWrapper.cpp @@ -4,6 +4,8 @@ #include "QtHttpReply.h" #include "QtHttpServer.h" #include "QtHttpHeader.h" +#include "WebSocketClient.h" +#include "WebJsonRpc.h" #include #include @@ -109,7 +111,18 @@ void QtHttpClientWrapper::onClientDataReceived (void) { } switch (m_parsingStatus) { // handle parsing status end/error case RequestParsed: { // a valid request has ben fully parsed - // add post data to request + // Catch websocket header "Upgrade" + if(m_currentRequest->getHeader(QtHttpHeader::Upgrade) == "websocket") + { + if(m_websocketClient == Q_NULLPTR) + { + // disconnect this slot from socket for further requests + disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); + m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, this); + } + break; + } + // add post data to request and catch /jsonrpc subroute url if ( m_currentRequest->getCommand() == "POST") { QtHttpPostData postData; @@ -126,12 +139,27 @@ void QtHttpClientWrapper::onClientDataReceived (void) { postData.insert(QString::fromUtf8(keyValue.at(0)),value); } m_currentRequest->setPostData(postData); + + // catch /jsonrpc in url, we need async callback, StaticFileServing is sync + QString path = m_currentRequest->getUrl ().path (); + QStringList uri_parts = path.split('/', QString::SkipEmptyParts); + if ( ! uri_parts.empty() && uri_parts.at(0) == "json-rpc" ) + { + if(m_webJsonRpc == Q_NULLPTR) + { + m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, this); + } + m_webJsonRpc->handleMessage(m_currentRequest); + break; + } } + QtHttpReply reply (m_serverHandle); connect (&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested); connect (&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested); + emit m_serverHandle->requestNeedsReply (m_currentRequest, &reply); // allow app to handle request m_parsingStatus = sendReplyToClient (&reply); break; @@ -201,8 +229,16 @@ void QtHttpClientWrapper::onReplySendDataRequested (void) { } } +void QtHttpClientWrapper::sendToClientWithReply(QtHttpReply * reply) { + connect (reply, &QtHttpReply::requestSendHeaders, + this, &QtHttpClientWrapper::onReplySendHeadersRequested); + connect (reply, &QtHttpReply::requestSendData, + this, &QtHttpClientWrapper::onReplySendDataRequested); + m_parsingStatus = sendReplyToClient (reply); +} + QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient (QtHttpReply * reply) { - if (reply != Q_NULLPTR) { + if (reply != Q_NULLPTR) { if (!reply->useChunked ()) { //reply->appendRawData (CRLF); // send all headers and all data in one shot diff --git a/libsrc/webconfig/QtHttpClientWrapper.h b/libsrc/webconfig/QtHttpClientWrapper.h index ae68a329..3e2c3a35 100644 --- a/libsrc/webconfig/QtHttpClientWrapper.h +++ b/libsrc/webconfig/QtHttpClientWrapper.h @@ -9,6 +9,8 @@ class QTcpSocket; class QtHttpRequest; class QtHttpReply; class QtHttpServer; +class WebSocketClient; +class WebJsonRpc; class QtHttpClientWrapper : public QObject { Q_OBJECT @@ -29,6 +31,8 @@ public: }; QString getGuid (void); + /// @brief Wrapper for sendReplyToClient(), handles m_parsingStatus and signal connect + void sendToClientWithReply (QtHttpReply * reply); private slots: void onClientDataReceived (void); @@ -41,11 +45,13 @@ protected slots: void onReplySendDataRequested (void); private: - QString m_guid; - ParsingStatus m_parsingStatus; - QTcpSocket * m_sockClient; - QtHttpRequest * m_currentRequest; - QtHttpServer * m_serverHandle; + QString m_guid; + ParsingStatus m_parsingStatus; + QTcpSocket * m_sockClient; + QtHttpRequest * m_currentRequest; + QtHttpServer * m_serverHandle; + WebSocketClient * m_websocketClient = nullptr; + WebJsonRpc * m_webJsonRpc = nullptr; }; #endif // QTHTTPCLIENTWRAPPER_H diff --git a/libsrc/webconfig/QtHttpHeader.cpp b/libsrc/webconfig/QtHttpHeader.cpp index c3f4eb55..248b51ad 100644 --- a/libsrc/webconfig/QtHttpHeader.cpp +++ b/libsrc/webconfig/QtHttpHeader.cpp @@ -3,30 +3,34 @@ #include -const QByteArray & QtHttpHeader::Server = QByteArrayLiteral ("Server"); -const QByteArray & QtHttpHeader::Date = QByteArrayLiteral ("Date"); -const QByteArray & QtHttpHeader::Host = QByteArrayLiteral ("Host"); -const QByteArray & QtHttpHeader::Accept = QByteArrayLiteral ("Accept"); -const QByteArray & QtHttpHeader::Cookie = QByteArrayLiteral ("Cookie"); -const QByteArray & QtHttpHeader::ContentType = QByteArrayLiteral ("Content-Type"); -const QByteArray & QtHttpHeader::ContentLength = QByteArrayLiteral ("Content-Length"); -const QByteArray & QtHttpHeader::Connection = QByteArrayLiteral ("Connection"); -const QByteArray & QtHttpHeader::UserAgent = QByteArrayLiteral ("User-Agent"); -const QByteArray & QtHttpHeader::AcceptCharset = QByteArrayLiteral ("Accept-Charset"); -const QByteArray & QtHttpHeader::AcceptEncoding = QByteArrayLiteral ("Accept-Encoding"); -const QByteArray & QtHttpHeader::AcceptLanguage = QByteArrayLiteral ("Accept-Language"); -const QByteArray & QtHttpHeader::Authorization = QByteArrayLiteral ("Authorization"); -const QByteArray & QtHttpHeader::CacheControl = QByteArrayLiteral ("Cache-Control"); -const QByteArray & QtHttpHeader::ContentMD5 = QByteArrayLiteral ("Content-MD5"); -const QByteArray & QtHttpHeader::ProxyAuthorization = QByteArrayLiteral ("Proxy-Authorization"); -const QByteArray & QtHttpHeader::Range = QByteArrayLiteral ("Range"); -const QByteArray & QtHttpHeader::ContentEncoding = QByteArrayLiteral ("Content-Encoding"); -const QByteArray & QtHttpHeader::ContentLanguage = QByteArrayLiteral ("Content-Language"); -const QByteArray & QtHttpHeader::ContentLocation = QByteArrayLiteral ("Content-Location"); -const QByteArray & QtHttpHeader::ContentRange = QByteArrayLiteral ("Content-Range"); -const QByteArray & QtHttpHeader::Expires = QByteArrayLiteral ("Expires"); -const QByteArray & QtHttpHeader::LastModified = QByteArrayLiteral ("Last-Modified"); -const QByteArray & QtHttpHeader::Location = QByteArrayLiteral ("Location"); -const QByteArray & QtHttpHeader::SetCookie = QByteArrayLiteral ("Set-Cookie"); -const QByteArray & QtHttpHeader::TransferEncoding = QByteArrayLiteral ("Transfer-Encoding"); -const QByteArray & QtHttpHeader::ContentDisposition = QByteArrayLiteral ("Content-Disposition"); +const QByteArray & QtHttpHeader::Server = QByteArrayLiteral ("Server"); +const QByteArray & QtHttpHeader::Date = QByteArrayLiteral ("Date"); +const QByteArray & QtHttpHeader::Host = QByteArrayLiteral ("Host"); +const QByteArray & QtHttpHeader::Accept = QByteArrayLiteral ("Accept"); +const QByteArray & QtHttpHeader::Cookie = QByteArrayLiteral ("Cookie"); +const QByteArray & QtHttpHeader::ContentType = QByteArrayLiteral ("Content-Type"); +const QByteArray & QtHttpHeader::ContentLength = QByteArrayLiteral ("Content-Length"); +const QByteArray & QtHttpHeader::Connection = QByteArrayLiteral ("Connection"); +const QByteArray & QtHttpHeader::UserAgent = QByteArrayLiteral ("User-Agent"); +const QByteArray & QtHttpHeader::AcceptCharset = QByteArrayLiteral ("Accept-Charset"); +const QByteArray & QtHttpHeader::AcceptEncoding = QByteArrayLiteral ("Accept-Encoding"); +const QByteArray & QtHttpHeader::AcceptLanguage = QByteArrayLiteral ("Accept-Language"); +const QByteArray & QtHttpHeader::Authorization = QByteArrayLiteral ("Authorization"); +const QByteArray & QtHttpHeader::CacheControl = QByteArrayLiteral ("Cache-Control"); +const QByteArray & QtHttpHeader::ContentMD5 = QByteArrayLiteral ("Content-MD5"); +const QByteArray & QtHttpHeader::ProxyAuthorization = QByteArrayLiteral ("Proxy-Authorization"); +const QByteArray & QtHttpHeader::Range = QByteArrayLiteral ("Range"); +const QByteArray & QtHttpHeader::ContentEncoding = QByteArrayLiteral ("Content-Encoding"); +const QByteArray & QtHttpHeader::ContentLanguage = QByteArrayLiteral ("Content-Language"); +const QByteArray & QtHttpHeader::ContentLocation = QByteArrayLiteral ("Content-Location"); +const QByteArray & QtHttpHeader::ContentRange = QByteArrayLiteral ("Content-Range"); +const QByteArray & QtHttpHeader::Expires = QByteArrayLiteral ("Expires"); +const QByteArray & QtHttpHeader::LastModified = QByteArrayLiteral ("Last-Modified"); +const QByteArray & QtHttpHeader::Location = QByteArrayLiteral ("Location"); +const QByteArray & QtHttpHeader::SetCookie = QByteArrayLiteral ("Set-Cookie"); +const QByteArray & QtHttpHeader::TransferEncoding = QByteArrayLiteral ("Transfer-Encoding"); +const QByteArray & QtHttpHeader::ContentDisposition = QByteArrayLiteral ("Content-Disposition"); +const QByteArray & QtHttpHeader::Upgrade = QByteArrayLiteral ("Upgrade"); +const QByteArray & QtHttpHeader::SecWebSocketKey = QByteArrayLiteral ("Sec-WebSocket-Key"); +const QByteArray & QtHttpHeader::SecWebSocketProtocol = QByteArrayLiteral ("Sec-WebSocket-Protocol"); +const QByteArray & QtHttpHeader::SecWebSocketVersion = QByteArrayLiteral ("Sec-WebSocket-Version"); diff --git a/libsrc/webconfig/QtHttpHeader.h b/libsrc/webconfig/QtHttpHeader.h index 9728414a..6bc03565 100644 --- a/libsrc/webconfig/QtHttpHeader.h +++ b/libsrc/webconfig/QtHttpHeader.h @@ -32,6 +32,11 @@ public: static const QByteArray & SetCookie; static const QByteArray & TransferEncoding; static const QByteArray & ContentDisposition; + // Websocket specific headers + static const QByteArray & Upgrade; + static const QByteArray & SecWebSocketKey; + static const QByteArray & SecWebSocketProtocol; + static const QByteArray & SecWebSocketVersion; }; #endif // QTHTTPHEADER_H diff --git a/libsrc/webconfig/QtHttpServer.cpp b/libsrc/webconfig/QtHttpServer.cpp index 2830b8f7..89207434 100644 --- a/libsrc/webconfig/QtHttpServer.cpp +++ b/libsrc/webconfig/QtHttpServer.cpp @@ -4,7 +4,6 @@ #include "QtHttpReply.h" #include "QtHttpClientWrapper.h" -#include #include const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1"); diff --git a/libsrc/webconfig/StaticFileServing.cpp b/libsrc/webconfig/StaticFileServing.cpp index 979e575a..80e8e901 100644 --- a/libsrc/webconfig/StaticFileServing.cpp +++ b/libsrc/webconfig/StaticFileServing.cpp @@ -34,7 +34,6 @@ StaticFileServing::StaticFileServing (Hyperion *hyperion, QString baseUrl, quint connect (_server, &QtHttpServer::requestNeedsReply, this, &StaticFileServing::onRequestNeedsReply); _server->start (port); - } StaticFileServing::~StaticFileServing () @@ -60,14 +59,10 @@ void StaticFileServing::onServerStarted (quint16 port) txtRecord ); Debug(_log, "Web Config mDNS responder started"); - - // json-rpc for http - _jsonProcessor = new JsonProcessor(QString("HTTP-API"), _log, true); } void StaticFileServing::onServerStopped () { Info(_log, "stopped %s", _server->getServerName().toStdString().c_str()); - delete _jsonProcessor; } void StaticFileServing::onServerError (QString msg) @@ -113,50 +108,30 @@ void StaticFileServing::printErrorToReply (QtHttpReply * reply, QtHttpReply::Sta void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpReply * reply) { QString command = request->getCommand (); - if (command == QStringLiteral ("GET") || command == QStringLiteral ("POST")) + if (command == QStringLiteral ("GET")) { QString path = request->getUrl ().path (); QStringList uri_parts = path.split('/', QString::SkipEmptyParts); // special uri handling for server commands - if ( ! uri_parts.empty() ) + if ( ! uri_parts.empty() && uri_parts.at(0) == "cgi" ) { - if(uri_parts.at(0) == "cgi") + uri_parts.removeAt(0); + try { - uri_parts.removeAt(0); - try - { - _cgi.exec(uri_parts, request, reply); - } - catch(int err) - { - Error(_log,"Exception while executing cgi %s : %d", path.toStdString().c_str(), err); - printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); - } - catch(std::exception &e) - { - Error(_log,"Exception while executing cgi %s : %s", path.toStdString().c_str(), e.what()); - printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); - } - return; + _cgi.exec(uri_parts, request, reply); } - else if ( uri_parts.at(0) == "json-rpc" ) + catch(int err) { - QMetaObject::Connection m_connection; - QByteArray data = request->getRawData(); - QtHttpRequest::ClientInfo info = request->getClientInfo(); - - m_connection = QObject::connect(_jsonProcessor, &JsonProcessor::callbackMessage, - [reply](QJsonObject result) { - QJsonDocument doc(result); - reply->addHeader ("Content-Type", "application/json"); - reply->appendRawData (doc.toJson()); - }); - - _jsonProcessor->handleMessage(data,info.clientAddress.toString()); - QObject::disconnect( m_connection ); - return; + Error(_log,"Exception while executing cgi %s : %d", path.toStdString().c_str(), err); + printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); } + catch(std::exception &e) + { + Error(_log,"Exception while executing cgi %s : %s", path.toStdString().c_str(), e.what()); + printErrorToReply (reply, QtHttpReply::InternalError, "script failed (" % path % ")"); + } + return; } Q_INIT_RESOURCE(WebConfig); diff --git a/libsrc/webconfig/StaticFileServing.h b/libsrc/webconfig/StaticFileServing.h index 73069c4c..42e03950 100644 --- a/libsrc/webconfig/StaticFileServing.h +++ b/libsrc/webconfig/StaticFileServing.h @@ -12,8 +12,6 @@ #include #include -#include - class StaticFileServing : public QObject { Q_OBJECT @@ -35,7 +33,6 @@ private: QMimeDatabase * _mimeDb; CgiHandler _cgi; Logger * _log; - JsonProcessor * _jsonProcessor; void printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage); diff --git a/libsrc/webconfig/WebJsonRpc.cpp b/libsrc/webconfig/WebJsonRpc.cpp new file mode 100644 index 00000000..16f4a52b --- /dev/null +++ b/libsrc/webconfig/WebJsonRpc.cpp @@ -0,0 +1,38 @@ +#include "WebJsonRpc.h" +#include "QtHttpReply.h" +#include "QtHttpRequest.h" +#include "QtHttpServer.h" +#include "QtHttpClientWrapper.h" + +#include + +WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent) + : QObject(parent) + , _server(server) + , _wrapper(parent) + , _log(Logger::getInstance("HTTPJSONRPC")) +{ + const QString client = request->getClientInfo().clientAddress.toString(); + _jsonProcessor = new JsonProcessor(client, _log, this, true); + connect(_jsonProcessor, &JsonProcessor::callbackMessage, this, &WebJsonRpc::handleCallback); +} + +void WebJsonRpc::handleMessage(QtHttpRequest* request) +{ + QByteArray data = request->getRawData(); + _unlocked = true; + _jsonProcessor->handleMessage(data); +} + +void WebJsonRpc::handleCallback(QJsonObject obj) +{ + // guard against wrong callbacks; TODO: Remove when JsonProcessor is more solid + if(!_unlocked) return; + _unlocked = false; + // construct reply with headers timestamp and server name + QtHttpReply reply(_server); + QJsonDocument doc(obj); + reply.addHeader ("Content-Type", "application/json"); + reply.appendRawData (doc.toJson()); + _wrapper->sendToClientWithReply(&reply); +} diff --git a/libsrc/webconfig/WebJsonRpc.h b/libsrc/webconfig/WebJsonRpc.h new file mode 100644 index 00000000..5aabfa71 --- /dev/null +++ b/libsrc/webconfig/WebJsonRpc.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class QtHttpServer; +class QtHttpRequest; +class QtHttpClientWrapper; +class JsonProcessor; + +class WebJsonRpc : public QObject { + Q_OBJECT +public: + WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent); + + void handleMessage(QtHttpRequest* request); + +private: + QtHttpServer* _server; + QtHttpClientWrapper* _wrapper; + Logger* _log; + JsonProcessor* _jsonProcessor; + + bool _unlocked = false; + +private slots: + void handleCallback(QJsonObject obj); +}; diff --git a/libsrc/webconfig/WebSocketClient.cpp b/libsrc/webconfig/WebSocketClient.cpp new file mode 100644 index 00000000..c24a796d --- /dev/null +++ b/libsrc/webconfig/WebSocketClient.cpp @@ -0,0 +1,336 @@ +#include "WebSocketClient.h" +#include "QtHttpRequest.h" +#include "QtHttpHeader.h" + +#include +#include + +#include +#include +#include + +WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, QObject* parent) + : QObject(parent) + , _socket(sock) + , _log(Logger::getInstance("WEBSOCKET")) + , _hyperion(Hyperion::getInstance()) +{ + // 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 + _jsonProcessor = new JsonProcessor(client, _log, this); + connect(_jsonProcessor, &JsonProcessor::callbackMessage, this, &WebSocketClient::sendMessage); + + 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(); +} + +void WebSocketClient::handleWebSocketFrame(void) +{ + // we are on no continious reading from socket from call before + if (!_notEnoughData) + { + getWsFrameHeader(&_wsh); + } + + if(_socket->bytesAvailable() < (qint64)_wsh.payloadLength) + { + //printf("not enough data %llu %llu\n", _socket->bytesAvailable(), _wsh.payloadLength); + _notEnoughData=true; + return; + } + _notEnoughData = false; + + QByteArray buf = _socket->read(_wsh.payloadLength); + //printf("opcode %x payload bytes %llu avail: %llu\n", _wsh.opCode, _wsh.payloadLength, _socket->bytesAvailable()); + + 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: + { + // check for protocal 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 (_wsh.opCode == OPCODE::TEXT) + { + _jsonProcessor->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, QString reason) +{ + Debug(_log, "send close: %d %s", status, QSTRING_CSTR(reason)); + ErrorIf(!reason.isEmpty(), _log, 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); + + _socket->write(sendBuffer); + _socket->flush(); + _socket->close(); +} + +void WebSocketClient::handleBinaryMessage(QByteArray &data) +{ + uint8_t priority = data.at(0); + unsigned duration_s = data.at(1); + unsigned imgSize = data.size() - 4; + unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF); + unsigned height = imgSize / width; + + if ( ! (imgSize) % width) + { + Error(_log, "data size is not multiple of width"); + return; + } + + Image image; + image.resize(width, height); + + memcpy(image.memptr(), data.data()+4, imgSize); + _hyperion->setImage(priority, image, duration_s*1000); +} + +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(OPCODE::TEXT, 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/webconfig/WebSocketClient.h b/libsrc/webconfig/WebSocketClient.h new file mode 100644 index 00000000..7aaf3ce3 --- /dev/null +++ b/libsrc/webconfig/WebSocketClient.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include "WebSocketUtils.h" + +class QTcpSocket; + +class QtHttpRequest; +class Hyperion; +class JsonProcessor; + +class WebSocketClient : public QObject { + Q_OBJECT +public: + WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, QObject* parent); + + struct WebSocketHeader + { + bool fin; + quint8 opCode; + bool masked; + quint64 payloadLength; + char key[4]; + }; + +private: + QTcpSocket* _socket; + Logger* _log; + Hyperion* _hyperion; + JsonProcessor* _jsonProcessor; + + void getWsFrameHeader(WebSocketHeader* header); + void sendClose(int status, 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; + + // 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(void); + qint64 sendMessage(QJsonObject obj); +}; diff --git a/libsrc/webconfig/WebSocketUtils.h b/libsrc/webconfig/WebSocketUtils.h new file mode 100644 index 00000000..271431f5 --- /dev/null +++ b/libsrc/webconfig/WebSocketUtils.h @@ -0,0 +1,68 @@ +#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 + }; +}