diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 18:07:22 +0000 |
commit | c04dcc2e7d834218ef2d4194331e383402495ae1 (patch) | |
tree | 7333e38d10d75386e60f336b80c2443c1166031d /xbmc/network/websocket | |
parent | Initial commit. (diff) | |
download | kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip |
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | xbmc/network/websocket/CMakeLists.txt | 11 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocket.cpp | 430 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocket.h | 136 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocketManager.cpp | 77 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocketManager.h | 19 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocketV13.cpp | 154 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocketV13.h | 22 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocketV8.cpp | 189 | ||||
-rw-r--r-- | xbmc/network/websocket/WebSocketV8.h | 33 |
9 files changed, 1071 insertions, 0 deletions
diff --git a/xbmc/network/websocket/CMakeLists.txt b/xbmc/network/websocket/CMakeLists.txt new file mode 100644 index 0000000..306cd6c --- /dev/null +++ b/xbmc/network/websocket/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES WebSocket.cpp + WebSocketManager.cpp + WebSocketV13.cpp + WebSocketV8.cpp) + +set(HEADERS WebSocket.h + WebSocketManager.h + WebSocketV13.h + WebSocketV8.h) + +core_add_library(network_websockets) diff --git a/xbmc/network/websocket/WebSocket.cpp b/xbmc/network/websocket/WebSocket.cpp new file mode 100644 index 0000000..bbf4a01 --- /dev/null +++ b/xbmc/network/websocket/WebSocket.cpp @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WebSocket.h" + +#include "utils/EndianSwap.h" +#include "utils/HttpParser.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <sstream> +#include <string> + +#define MASK_FIN 0x80 +#define MASK_RSV1 0x40 +#define MASK_RSV2 0x20 +#define MASK_RSV3 0x10 +#define MASK_RSV (MASK_RSV1 | MASK_RSV2 | MASK_RSV3) +#define MASK_OPCODE 0x0F +#define MASK_MASK 0x80 +#define MASK_LENGTH 0x7F + +#define CONTROL_FRAME 0x08 + +#define LENGTH_MIN 0x2 + +CWebSocketFrame::CWebSocketFrame(const char* data, uint64_t length) +{ + reset(); + + if (data == NULL || length < LENGTH_MIN) + return; + + m_free = false; + m_data = data; + m_lengthFrame = length; + + // Get the FIN flag + m_final = ((m_data[0] & MASK_FIN) == MASK_FIN); + // Get the RSV1 - RSV3 flags + m_extension |= m_data[0] & MASK_RSV1; + m_extension |= (m_data[0] & MASK_RSV2) << 1; + m_extension |= (m_data[0] & MASK_RSV3) << 2; + // Get the opcode + m_opcode = (WebSocketFrameOpcode)(m_data[0] & MASK_OPCODE); + if (m_opcode >= WebSocketUnknownFrame) + { + CLog::Log(LOGINFO, "WebSocket: Frame with invalid opcode {:2X} received", m_opcode); + reset(); + return; + } + if ((m_opcode & CONTROL_FRAME) == CONTROL_FRAME && !m_final) + { + CLog::Log(LOGINFO, "WebSocket: Fragmented control frame (opcode {:2X}) received", m_opcode); + reset(); + return; + } + + // Get the MASK flag + m_masked = ((m_data[1] & MASK_MASK) == MASK_MASK); + + // Get the payload length + m_length = (uint64_t)(m_data[1] & MASK_LENGTH); + if ((m_length <= 125 && m_lengthFrame < m_length + LENGTH_MIN) || + (m_length == 126 && m_lengthFrame < LENGTH_MIN + 2) || + (m_length == 127 && m_lengthFrame < LENGTH_MIN + 8)) + { + CLog::Log(LOGINFO, "WebSocket: Frame with invalid length received"); + reset(); + return; + } + + if (IsControlFrame() && (m_length > 125 || !m_final)) + { + CLog::Log(LOGWARNING, "WebSocket: Invalid control frame received"); + reset(); + return; + } + + int offset = 0; + if (m_length == 126) + { + m_length = (uint64_t)Endian_SwapBE16(*(const uint16_t *)(m_data + 2)); + offset = 2; + } + else if (m_length == 127) + { + m_length = Endian_SwapBE64(*(const uint64_t *)(m_data + 2)); + offset = 8; + } + + if (m_lengthFrame < LENGTH_MIN + offset + m_length) + { + CLog::Log(LOGINFO, "WebSocket: Frame with invalid length received"); + reset(); + return; + } + + // Get the mask + if (m_masked) + { + m_mask = *(const uint32_t *)(m_data + LENGTH_MIN + offset); + offset += 4; + } + + if (m_lengthFrame != LENGTH_MIN + offset + m_length) + m_lengthFrame = LENGTH_MIN + offset + m_length; + + // Get application data + if (m_length > 0) + m_applicationData = const_cast<char *>(m_data + LENGTH_MIN + offset); + else + m_applicationData = NULL; + + // Unmask the application data if necessary + if (m_masked) + { + for (uint64_t index = 0; index < m_length; index++) + m_applicationData[index] = m_applicationData[index] ^ ((char *)(&m_mask))[index % 4]; + } + + m_valid = true; +} + +CWebSocketFrame::CWebSocketFrame(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */, + bool final /* = true */, bool masked /* = false */, int32_t mask /* = 0 */, int8_t extension /* = 0 */) +{ + reset(); + + if (opcode >= WebSocketUnknownFrame) + return; + + m_free = true; + m_opcode = opcode; + + m_length = length; + + m_masked = masked; + m_mask = mask; + m_final = final; + m_extension = extension; + + std::string buffer; + char dataByte = 0; + + // Set the FIN flag + if (m_final) + dataByte |= MASK_FIN; + + // Set RSV1 - RSV3 flags + if (m_extension != 0) + dataByte |= (m_extension << 4) & MASK_RSV; + + // Set opcode flag + dataByte |= opcode & MASK_OPCODE; + + buffer.push_back(dataByte); + dataByte = 0; + + // Set MASK flag + if (m_masked) + dataByte |= MASK_MASK; + + // Set payload length + if (m_length < 126) + { + dataByte |= m_length & MASK_LENGTH; + buffer.push_back(dataByte); + } + else if (m_length <= 65535) + { + dataByte |= 126 & MASK_LENGTH; + buffer.push_back(dataByte); + + uint16_t dataLength = Endian_SwapBE16((uint16_t)m_length); + buffer.append((const char*)&dataLength, 2); + } + else + { + dataByte |= 127 & MASK_LENGTH; + buffer.push_back(dataByte); + + uint64_t dataLength = Endian_SwapBE64(m_length); + buffer.append((const char*)&dataLength, 8); + } + + uint64_t applicationDataOffset = 0; + if (data) + { + // Set masking key + if (m_masked) + { + buffer.append((char *)&m_mask, sizeof(m_mask)); + applicationDataOffset = buffer.size(); + + for (uint64_t index = 0; index < m_length; index++) + buffer.push_back(data[index] ^ ((char *)(&m_mask))[index % 4]); + } + else + { + applicationDataOffset = buffer.size(); + buffer.append(data, (unsigned int)length); + } + } + + // Get the whole data + m_lengthFrame = buffer.size(); + m_data = new char[(uint32_t)m_lengthFrame]; + memcpy(const_cast<char *>(m_data), buffer.c_str(), (uint32_t)m_lengthFrame); + + if (data) + { + m_applicationData = const_cast<char *>(m_data); + m_applicationData += applicationDataOffset; + } + + m_valid = true; +} + +CWebSocketFrame::~CWebSocketFrame() +{ + if (!m_valid) + return; + + if (m_free && m_data != NULL) + { + delete[] m_data; + m_data = NULL; + } +} + +void CWebSocketFrame::reset() +{ + m_free = false; + m_data = NULL; + m_lengthFrame = 0; + m_length = 0; + m_valid = false; + m_final = false; + m_extension = 0; + m_opcode = WebSocketUnknownFrame; + m_masked = false; + m_mask = 0; + m_applicationData = NULL; +} + +CWebSocketMessage::CWebSocketMessage() +{ + Clear(); +} + +CWebSocketMessage::~CWebSocketMessage() +{ + for (unsigned int index = 0; index < m_frames.size(); index++) + delete m_frames[index]; + + m_frames.clear(); +} + +bool CWebSocketMessage::AddFrame(const CWebSocketFrame *frame) +{ + if (!frame->IsValid() || m_complete) + return false; + + if (frame->IsFinal()) + m_complete = true; + else + m_fragmented = true; + + m_frames.push_back(frame); + + return true; +} + +void CWebSocketMessage::Clear() +{ + m_fragmented = false; + m_complete = false; + + m_frames.clear(); +} + +const CWebSocketMessage* CWebSocket::Handle(const char* &buffer, size_t &length, bool &send) +{ + send = false; + + while (length > 0) + { + switch (m_state) + { + case WebSocketStateConnected: + { + CWebSocketFrame *frame = GetFrame(buffer, length); + if (!frame->IsValid()) + { + CLog::Log(LOGINFO, "WebSocket: Invalid frame received"); + delete frame; + return NULL; + } + + // adjust the length and the buffer values + length -= (size_t)frame->GetFrameLength(); + buffer += frame->GetFrameLength(); + + if (frame->IsControlFrame()) + { + if (!frame->IsFinal()) + { + delete frame; + return NULL; + } + + CWebSocketMessage *msg = NULL; + switch (frame->GetOpcode()) + { + case WebSocketPing: + msg = GetMessage(); + if (msg != NULL) + msg->AddFrame(Pong(frame->GetApplicationData())); + break; + + case WebSocketConnectionClose: + CLog::Log(LOGINFO, "WebSocket: connection closed by client"); + + msg = GetMessage(); + if (msg != NULL) + msg->AddFrame(Close()); + + m_state = WebSocketStateClosed; + break; + + case WebSocketContinuationFrame: + case WebSocketTextFrame: + case WebSocketBinaryFrame: + case WebSocketPong: + case WebSocketUnknownFrame: + default: + break; + } + + delete frame; + + if (msg != NULL) + send = true; + + return msg; + } + + if (m_message == NULL && (m_message = GetMessage()) == NULL) + { + CLog::Log(LOGINFO, "WebSocket: Could not allocate a new websocket message"); + delete frame; + return NULL; + } + + m_message->AddFrame(frame); + if (!m_message->IsComplete()) + { + if (length > 0) + continue; + else + return NULL; + } + + CWebSocketMessage *msg = m_message; + m_message = NULL; + return msg; + } + + case WebSocketStateClosing: + { + CWebSocketFrame *frame = GetFrame(buffer, length); + + if (frame->IsValid()) + { + // adjust the length and the buffer values + length -= (size_t)frame->GetFrameLength(); + buffer += frame->GetFrameLength(); + } + + if (!frame->IsValid() || frame->GetOpcode() == WebSocketConnectionClose) + { + CLog::Log(LOGINFO, "WebSocket: Invalid or unexpected frame received (only closing handshake expected)"); + delete frame; + return NULL; + } + + m_state = WebSocketStateClosed; + return NULL; + } + + case WebSocketStateNotConnected: + case WebSocketStateClosed: + case WebSocketStateHandshaking: + default: + CLog::Log(LOGINFO, "WebSocket: No frame expected in the current state"); + return NULL; + } + } + + return NULL; +} + +const CWebSocketMessage* CWebSocket::Send(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */) +{ + CWebSocketFrame *frame = GetFrame(opcode, data, length); + if (frame == NULL || !frame->IsValid()) + { + CLog::Log(LOGINFO, "WebSocket: Trying to send an invalid frame"); + return NULL; + } + + CWebSocketMessage *msg = GetMessage(); + if (msg == NULL) + { + CLog::Log(LOGINFO, "WebSocket: Could not allocate a message"); + return NULL; + } + + msg->AddFrame(frame); + if (msg->IsComplete()) + return msg; + + return NULL; +} diff --git a/xbmc/network/websocket/WebSocket.h b/xbmc/network/websocket/WebSocket.h new file mode 100644 index 0000000..8901ede --- /dev/null +++ b/xbmc/network/websocket/WebSocket.h @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <stdint.h> +#include <string> +#include <vector> + +enum WebSocketFrameOpcode +{ + WebSocketContinuationFrame = 0x00, + WebSocketTextFrame = 0x01, + WebSocketBinaryFrame = 0x02, + //0x3 - 0x7 are reserved for non-control frames + WebSocketConnectionClose = 0x08, + WebSocketPing = 0x09, + WebSocketPong = 0x0A, + //0xB - 0xF are reserved for control frames + WebSocketUnknownFrame = 0x10 +}; + +enum WebSocketState +{ + WebSocketStateNotConnected = 0, + WebSocketStateHandshaking = 1, + WebSocketStateConnected = 2, + WebSocketStateClosing = 3, + WebSocketStateClosed = 4 +}; + +enum WebSocketCloseReason +{ + WebSocketCloseNormal = 1000, + WebSocketCloseLeaving = 1001, + WebSocketCloseProtocolError = 1002, + WebSocketCloseInvalidData = 1003, + WebSocketCloseFrameTooLarge = 1004, + // Reserved status code = 1005, + // Reserved status code = 1006, + WebSocketCloseInvalidUtf8 = 1007 +}; + +class CWebSocketFrame +{ +public: + CWebSocketFrame(const char* data, uint64_t length); + CWebSocketFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0); + virtual ~CWebSocketFrame(); + + virtual bool IsValid() const { return m_valid; } + virtual uint64_t GetFrameLength() const { return m_lengthFrame; } + virtual bool IsFinal() const { return m_final; } + virtual int8_t GetExtension() const { return m_extension; } + virtual WebSocketFrameOpcode GetOpcode() const { return m_opcode; } + virtual bool IsControlFrame() const { return (m_valid && (m_opcode & 0x8) == 0x8); } + virtual bool IsMasked() const { return m_masked; } + virtual uint64_t GetLength() const { return m_length; } + virtual int32_t GetMask() const { return m_mask; } + virtual const char* GetFrameData() const { return m_data; } + virtual const char* GetApplicationData() const { return m_applicationData; } + +protected: + bool m_free; + const char *m_data; + uint64_t m_lengthFrame; + uint64_t m_length; + bool m_valid; + bool m_final; + int8_t m_extension; + WebSocketFrameOpcode m_opcode; + bool m_masked; + int32_t m_mask; + char *m_applicationData; + +private: + void reset(); + CWebSocketFrame(const CWebSocketFrame&) = delete; + CWebSocketFrame& operator=(const CWebSocketFrame&) = delete; +}; + +class CWebSocketMessage +{ +public: + CWebSocketMessage(); + virtual ~CWebSocketMessage(); + + virtual bool IsFragmented() const { return m_fragmented; } + virtual bool IsComplete() const { return m_complete; } + + virtual bool AddFrame(const CWebSocketFrame* frame); + virtual const std::vector<const CWebSocketFrame *>& GetFrames() const { return m_frames; } + + virtual void Clear(); + +protected: + std::vector<const CWebSocketFrame *> m_frames; + bool m_fragmented; + bool m_complete; +}; + +class CWebSocket +{ +public: + CWebSocket() { m_state = WebSocketStateNotConnected; m_message = NULL; } + virtual ~CWebSocket() + { + if (m_message) + delete m_message; + } + + int GetVersion() { return m_version; } + WebSocketState GetState() { return m_state; } + + virtual bool Handshake(const char* data, size_t length, std::string &response) = 0; + virtual const CWebSocketMessage* Handle(const char* &buffer, size_t &length, bool &send); + virtual const CWebSocketMessage* Send(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0); + virtual const CWebSocketFrame* Ping(const char* data = NULL) const = 0; + virtual const CWebSocketFrame* Pong(const char* data = NULL) const = 0; + virtual const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") = 0; + virtual void Fail() = 0; + +protected: + int m_version; + WebSocketState m_state; + CWebSocketMessage *m_message; + + virtual CWebSocketFrame* GetFrame(const char* data, uint64_t length) = 0; + virtual CWebSocketFrame* GetFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0) = 0; + virtual CWebSocketMessage* GetMessage() = 0; +}; diff --git a/xbmc/network/websocket/WebSocketManager.cpp b/xbmc/network/websocket/WebSocketManager.cpp new file mode 100644 index 0000000..6fef69a --- /dev/null +++ b/xbmc/network/websocket/WebSocketManager.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WebSocketManager.h" + +#include "WebSocket.h" +#include "WebSocketV13.h" +#include "WebSocketV8.h" +#include "utils/HttpParser.h" +#include "utils/HttpResponse.h" +#include "utils/log.h" + +#include <string> + +#define WS_HTTP_METHOD "GET" +#define WS_HTTP_TAG "HTTP/" +#define WS_SUPPORTED_VERSIONS "8, 13" + +#define WS_HEADER_VERSION "Sec-WebSocket-Version" +#define WS_HEADER_VERSION_LC "sec-websocket-version" // "Sec-WebSocket-Version" + +CWebSocket* CWebSocketManager::Handle(const char* data, unsigned int length, std::string &response) +{ + if (data == NULL || length <= 0) + return NULL; + + HttpParser header; + HttpParser::status_t status = header.addBytes(data, length); + switch (status) + { + case HttpParser::Error: + case HttpParser::Incomplete: + response.clear(); + return NULL; + + case HttpParser::Done: + default: + break; + } + + // There must be a "Sec-WebSocket-Version" header + const char* value = header.getValue(WS_HEADER_VERSION_LC); + if (value == NULL) + { + CLog::Log(LOGINFO, "WebSocket: missing Sec-WebSocket-Version"); + CHttpResponse httpResponse(HTTP::Get, HTTP::BadRequest, HTTP::Version1_1); + response = httpResponse.Create(); + + return NULL; + } + + CWebSocket *websocket = NULL; + if (strncmp(value, "8", 1) == 0) + websocket = new CWebSocketV8(); + else if (strncmp(value, "13", 2) == 0) + websocket = new CWebSocketV13(); + + if (websocket == NULL) + { + CLog::Log(LOGINFO, "WebSocket: Unsupported Sec-WebSocket-Version {}", value); + CHttpResponse httpResponse(HTTP::Get, HTTP::UpgradeRequired, HTTP::Version1_1); + httpResponse.AddHeader(WS_HEADER_VERSION, WS_SUPPORTED_VERSIONS); + response = httpResponse.Create(); + + return NULL; + } + + if (websocket->Handshake(data, length, response)) + return websocket; + + return NULL; +} diff --git a/xbmc/network/websocket/WebSocketManager.h b/xbmc/network/websocket/WebSocketManager.h new file mode 100644 index 0000000..7cbe8e9 --- /dev/null +++ b/xbmc/network/websocket/WebSocketManager.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <string> + +class CWebSocket; + +class CWebSocketManager +{ +public: + static CWebSocket* Handle(const char* data, unsigned int length, std::string &response); +}; diff --git a/xbmc/network/websocket/WebSocketV13.cpp b/xbmc/network/websocket/WebSocketV13.cpp new file mode 100644 index 0000000..e0ef8f0 --- /dev/null +++ b/xbmc/network/websocket/WebSocketV13.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WebSocketV13.h" + +#include "WebSocket.h" +#include "utils/HttpParser.h" +#include "utils/HttpResponse.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <algorithm> +#include <sstream> +#include <string> + +#define WS_HTTP_METHOD "GET" +#define WS_HTTP_TAG "HTTP/" + +#define WS_HEADER_UPGRADE "Upgrade" +#define WS_HEADER_UPGRADE_LC "upgrade" +#define WS_HEADER_CONNECTION "Connection" +#define WS_HEADER_CONNECTION_LC "connection" + +#define WS_HEADER_KEY_LC "sec-websocket-key" // "Sec-WebSocket-Key" +#define WS_HEADER_ACCEPT "Sec-WebSocket-Accept" +#define WS_HEADER_PROTOCOL "Sec-WebSocket-Protocol" +#define WS_HEADER_PROTOCOL_LC "sec-websocket-protocol" // "Sec-WebSocket-Protocol" + +#define WS_PROTOCOL_JSONRPC "jsonrpc.xbmc.org" +#define WS_HEADER_UPGRADE_VALUE "websocket" + +bool CWebSocketV13::Handshake(const char* data, size_t length, std::string &response) +{ + std::string strHeader(data, length); + const char *value; + HttpParser header; + if (header.addBytes(data, length) != HttpParser::Done) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: incomplete handshake received"); + return false; + } + + // The request must be GET + value = header.getMethod(); + if (value == NULL || + StringUtils::CompareNoCase(value, WS_HTTP_METHOD, strlen(WS_HTTP_METHOD)) != 0) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid HTTP method received (GET expected)"); + return false; + } + + // The request must be HTTP/1.1 or higher + size_t pos; + if ((pos = strHeader.find(WS_HTTP_TAG)) == std::string::npos) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid handshake received"); + return false; + } + + pos += strlen(WS_HTTP_TAG); + std::istringstream converter(strHeader.substr(pos, strHeader.find_first_of(" \r\n\t", pos) - pos)); + float fVersion; + converter >> fVersion; + + if (fVersion < 1.1f) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid HTTP version {:f} (1.1 or higher expected)", + fVersion); + return false; + } + + std::string websocketKey, websocketProtocol; + // There must be a "Host" header + value = header.getValue("host"); + if (value == NULL || strlen(value) == 0) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: \"Host\" header missing"); + return true; + } + + // There must be a "Upgrade" header with the value "websocket" + value = header.getValue(WS_HEADER_UPGRADE_LC); + if (value == NULL || StringUtils::CompareNoCase(value, WS_HEADER_UPGRADE_VALUE, + strlen(WS_HEADER_UPGRADE_VALUE)) != 0) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"{}\" received", WS_HEADER_UPGRADE); + return true; + } + + // There must be a "Connection" header with the value "Upgrade" + value = header.getValue(WS_HEADER_CONNECTION_LC); + std::vector<std::string> elements; + if (value != nullptr) + elements = StringUtils::Split(value, ","); + if (elements.empty() || !std::any_of(elements.begin(), elements.end(), [](std::string& elem) { return StringUtils::EqualsNoCase(StringUtils::Trim(elem), WS_HEADER_UPGRADE); })) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"{}\" received", WS_HEADER_CONNECTION_LC); + return true; + } + + // There must be a base64 encoded 16 byte (=> 24 byte as base62) "Sec-WebSocket-Key" header + value = header.getValue(WS_HEADER_KEY_LC); + if (value == NULL || (websocketKey = value).size() != 24) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"Sec-WebSocket-Key\" received"); + return true; + } + + // There might be a "Sec-WebSocket-Protocol" header + value = header.getValue(WS_HEADER_PROTOCOL_LC); + if (value && strlen(value) > 0) + { + std::vector<std::string> protocols = StringUtils::Split(value, ","); + for (auto& protocol : protocols) + { + StringUtils::Trim(protocol); + if (protocol == WS_PROTOCOL_JSONRPC) + { + websocketProtocol = WS_PROTOCOL_JSONRPC; + break; + } + } + } + + CHttpResponse httpResponse(HTTP::Get, HTTP::SwitchingProtocols, HTTP::Version1_1); + httpResponse.AddHeader(WS_HEADER_UPGRADE, WS_HEADER_UPGRADE_VALUE); + httpResponse.AddHeader(WS_HEADER_CONNECTION, WS_HEADER_UPGRADE); + std::string responseKey = calculateKey(websocketKey); + httpResponse.AddHeader(WS_HEADER_ACCEPT, responseKey); + if (!websocketProtocol.empty()) + httpResponse.AddHeader(WS_HEADER_PROTOCOL, websocketProtocol); + + response = httpResponse.Create(); + + m_state = WebSocketStateConnected; + + return true; +} + +const CWebSocketFrame* CWebSocketV13::Close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */) +{ + if (m_state == WebSocketStateNotConnected || m_state == WebSocketStateHandshaking || m_state == WebSocketStateClosed) + { + CLog::Log(LOGINFO, "WebSocket [RFC6455]: Cannot send a closing handshake if no connection has been established"); + return NULL; + } + + return close(reason, message); +} diff --git a/xbmc/network/websocket/WebSocketV13.h b/xbmc/network/websocket/WebSocketV13.h new file mode 100644 index 0000000..80c5a73 --- /dev/null +++ b/xbmc/network/websocket/WebSocketV13.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "WebSocketV8.h" + +#include <string> + +class CWebSocketV13 : public CWebSocketV8 +{ +public: + CWebSocketV13() { m_version = 13; } + + bool Handshake(const char* data, size_t length, std::string &response) override; + const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") override; +}; diff --git a/xbmc/network/websocket/WebSocketV8.cpp b/xbmc/network/websocket/WebSocketV8.cpp new file mode 100644 index 0000000..e0a187b --- /dev/null +++ b/xbmc/network/websocket/WebSocketV8.cpp @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "WebSocketV8.h" + +#include "WebSocket.h" +#include "utils/Base64.h" +#include "utils/Digest.h" +#include "utils/EndianSwap.h" +#include "utils/HttpParser.h" +#include "utils/HttpResponse.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <sstream> +#include <string> + +using KODI::UTILITY::CDigest; + +#define WS_HTTP_METHOD "GET" +#define WS_HTTP_TAG "HTTP/" + +#define WS_HEADER_UPGRADE "Upgrade" +#define WS_HEADER_CONNECTION "Connection" + +#define WS_HEADER_KEY_LC "sec-websocket-key" // "Sec-WebSocket-Key" +#define WS_HEADER_ACCEPT "Sec-WebSocket-Accept" +#define WS_HEADER_PROTOCOL "Sec-WebSocket-Protocol" +#define WS_HEADER_PROTOCOL_LC "sec-websocket-protocol" // "Sec-WebSocket-Protocol" + +#define WS_PROTOCOL_JSONRPC "jsonrpc.xbmc.org" +#define WS_HEADER_UPGRADE_VALUE "websocket" +#define WS_KEY_MAGICSTRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +bool CWebSocketV8::Handshake(const char* data, size_t length, std::string &response) +{ + std::string strHeader(data, length); + const char *value; + HttpParser header; + if (header.addBytes(data, length) != HttpParser::Done) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: incomplete handshake received"); + return false; + } + + // The request must be GET + value = header.getMethod(); + if (value == NULL || + StringUtils::CompareNoCase(value, WS_HTTP_METHOD, strlen(WS_HTTP_METHOD)) != 0) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid HTTP method received (GET expected)"); + return false; + } + + // The request must be HTTP/1.1 or higher + size_t pos; + if ((pos = strHeader.find(WS_HTTP_TAG)) == std::string::npos) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid handshake received"); + return false; + } + + pos += strlen(WS_HTTP_TAG); + std::istringstream converter(strHeader.substr(pos, strHeader.find_first_of(" \r\n\t", pos) - pos)); + float fVersion; + converter >> fVersion; + + if (fVersion < 1.1f) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid HTTP version {:f} (1.1 or higher expected)", + fVersion); + return false; + } + + std::string websocketKey, websocketProtocol; + // There must be a "Host" header + value = header.getValue("host"); + if (value == NULL || strlen(value) == 0) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: \"Host\" header missing"); + return true; + } + + // There must be a base64 encoded 16 byte (=> 24 byte as base64) "Sec-WebSocket-Key" header + value = header.getValue(WS_HEADER_KEY_LC); + if (value == NULL || (websocketKey = value).size() != 24) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid \"Sec-WebSocket-Key\" received"); + return true; + } + + // There might be a "Sec-WebSocket-Protocol" header + value = header.getValue(WS_HEADER_PROTOCOL_LC); + if (value && strlen(value) > 0) + { + std::vector<std::string> protocols = StringUtils::Split(value, ","); + for (auto& protocol : protocols) + { + StringUtils::Trim(protocol); + if (protocol == WS_PROTOCOL_JSONRPC) + { + websocketProtocol = WS_PROTOCOL_JSONRPC; + break; + } + } + } + + CHttpResponse httpResponse(HTTP::Get, HTTP::SwitchingProtocols, HTTP::Version1_1); + httpResponse.AddHeader(WS_HEADER_UPGRADE, WS_HEADER_UPGRADE_VALUE); + httpResponse.AddHeader(WS_HEADER_CONNECTION, WS_HEADER_UPGRADE); + httpResponse.AddHeader(WS_HEADER_ACCEPT, calculateKey(websocketKey)); + if (!websocketProtocol.empty()) + httpResponse.AddHeader(WS_HEADER_PROTOCOL, websocketProtocol); + + response = httpResponse.Create(); + + m_state = WebSocketStateConnected; + + return true; +} + +const CWebSocketFrame* CWebSocketV8::Close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */) +{ + if (m_state == WebSocketStateNotConnected || m_state == WebSocketStateHandshaking || m_state == WebSocketStateClosed) + { + CLog::Log(LOGINFO, "WebSocket [hybi-10]: Cannot send a closing handshake if no connection has been established"); + return NULL; + } + + return close(reason, message); +} + +void CWebSocketV8::Fail() +{ + m_state = WebSocketStateClosed; +} + +CWebSocketFrame* CWebSocketV8::GetFrame(const char* data, uint64_t length) +{ + return new CWebSocketFrame(data, length); +} + +CWebSocketFrame* CWebSocketV8::GetFrame(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */, + bool final /* = true */, bool masked /* = false */, int32_t mask /* = 0 */, int8_t extension /* = 0 */) +{ + return new CWebSocketFrame(opcode, data, length, final, masked, mask, extension); +} + +CWebSocketMessage* CWebSocketV8::GetMessage() +{ + return new CWebSocketMessage(); +} + +const CWebSocketFrame* CWebSocketV8::close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */) +{ + size_t length = 2 + message.size(); + + char* data = new char[length + 1]; + memset(data, 0, length + 1); + uint16_t iReason = Endian_SwapBE16((uint16_t)reason); + memcpy(data, &iReason, 2); + message.copy(data + 2, message.size()); + + if (m_state == WebSocketStateConnected) + m_state = WebSocketStateClosing; + else + m_state = WebSocketStateClosed; + + CWebSocketFrame* frame = new CWebSocketFrame(WebSocketConnectionClose, data, length); + delete[] data; + + return frame; +} + +std::string CWebSocketV8::calculateKey(const std::string &key) +{ + std::string acceptKey = key; + acceptKey.append(WS_KEY_MAGICSTRING); + + CDigest digest{CDigest::Type::SHA1}; + digest.Update(acceptKey); + + return Base64::Encode(digest.FinalizeRaw()); +} diff --git a/xbmc/network/websocket/WebSocketV8.h b/xbmc/network/websocket/WebSocketV8.h new file mode 100644 index 0000000..d86688c --- /dev/null +++ b/xbmc/network/websocket/WebSocketV8.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "WebSocket.h" + +#include <string> + +class CWebSocketV8 : public CWebSocket +{ +public: + CWebSocketV8() { m_version = 8; } + + bool Handshake(const char* data, size_t length, std::string &response) override; + const CWebSocketFrame* Ping(const char* data = NULL) const override { return new CWebSocketFrame(WebSocketPing, data); } + const CWebSocketFrame* Pong(const char* data = NULL) const override { return new CWebSocketFrame(WebSocketPong, data); } + const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") override; + void Fail() override; + +protected: + CWebSocketFrame* GetFrame(const char* data, uint64_t length) override; + CWebSocketFrame* GetFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0) override; + CWebSocketMessage* GetMessage() override; + virtual const CWebSocketFrame* close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = ""); + + std::string calculateKey(const std::string &key); +}; |