/** * @file WSServer.cpp * @brief Multi-client WebSocket server implementation. */ #include "WSServer.h" #include "WSFrame.h" #include "SHA1.h" #include "Base64.h" #include "AdvancedErrorManagement.h" #include "Sleep.h" #include "Threads.h" #include "TimeoutType.h" #include #include #include namespace StreamHub { using MARTe::TimeoutType; using MARTe::Sleep; /*---------------------------------------------------------------------------*/ /* Thread trampolines */ /*---------------------------------------------------------------------------*/ struct AcceptThreadArg { WSServer *srv; }; struct ClientThreadArg { WSServer *srv; uint32 slot; }; static void AcceptThreadEntry(const void *arg) { WSServer *srv = reinterpret_cast( const_cast(arg))->srv; delete reinterpret_cast(arg); srv->AcceptLoop(); } static void ClientReadEntry(const void *arg) { const ClientThreadArg *a = reinterpret_cast(arg); WSServer *srv = a->srv; uint32 slot = a->slot; delete a; srv->ClientReadLoop(slot); } /*---------------------------------------------------------------------------*/ /* Tiny JSON helpers (no library, only what we need) */ /*---------------------------------------------------------------------------*/ /** Locate the first occurrence of pattern in s, return pointer or NULL. */ static const char *FindSubstr(const char *s, const char *pattern) { return strstr(s, pattern); } /*---------------------------------------------------------------------------*/ /* WSServer */ /*---------------------------------------------------------------------------*/ WSServer::WSServer() : numClients(0u), callback(static_cast(0)), running(false), acceptTid(MARTe::InvalidThreadIdentifier) { for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) { clients[i].sock = static_cast(0); clients[i].active = false; clients[i].readTid = MARTe::InvalidThreadIdentifier; } } WSServer::~WSServer() { (void) Stop(); } bool WSServer::Start(uint16 port, WSCommandCallback *cb) { if (running) { return true; } callback = cb; running = true; if (!tcpListener.Open()) { REPORT_ERROR_STATIC(MARTe::ErrorManagement::FatalError, "WSServer: Failed to open TCP listener socket."); running = false; return false; } if (!tcpListener.Listen(port, static_cast(WS_MAX_CLIENTS))) { REPORT_ERROR_STATIC(MARTe::ErrorManagement::FatalError, "WSServer: Failed to listen on port %u.", static_cast(port)); running = false; return false; } AcceptThreadArg *arg = new AcceptThreadArg(); arg->srv = this; acceptTid = MARTe::Threads::BeginThread( reinterpret_cast(AcceptThreadEntry), arg, MARTe::THREADS_DEFAULT_STACKSIZE, "WSAccept"); REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information, "WSServer: Listening on port %u.", static_cast(port)); return true; } bool WSServer::Stop() { if (!running) { return true; } running = false; Sleep::MSec(200u); /* Close all client connections — their read threads will exit on error */ (void) clientsMutex.FastLock(); for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) { if (clients[i].active && (clients[i].sock != static_cast(0))) { clients[i].sock->Close(); } } clientsMutex.FastUnLock(); Sleep::MSec(200u); tcpListener.Close(); Sleep::MSec(100u); /* Free any remaining slots */ (void) clientsMutex.FastLock(); for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) { if (clients[i].sock != static_cast(0)) { delete clients[i].sock; clients[i].sock = static_cast(0); clients[i].active = false; } } numClients = 0u; clientsMutex.FastUnLock(); return true; } /*---------------------------------------------------------------------------*/ /* Accept loop */ /*---------------------------------------------------------------------------*/ void WSServer::AcceptLoop() { while (running) { BasicTCPSocket *client = tcpListener.WaitConnection( TimeoutType(500u), static_cast(0)); if (client == static_cast(0)) { /* Timeout — loop again to check running flag */ continue; } /* HTTP upgrade */ if (!UpgradeHTTP(client)) { client->Close(); delete client; continue; } uint32 slot = AllocSlot(client); if (slot == WS_MAX_CLIENTS) { /* No room */ static const char *full = "HTTP/1.1 503 Service Unavailable\r\n\r\n"; uint32 l = static_cast(strlen(full)); (void) client->Write(full, l); client->Close(); delete client; continue; } /* Notify StreamHub */ if (callback != static_cast(0)) { callback->OnWSClientConnected(); } /* Start per-client read thread */ ClientThreadArg *arg = new ClientThreadArg(); arg->srv = this; arg->slot = slot; clients[slot].readTid = MARTe::Threads::BeginThread( reinterpret_cast(ClientReadEntry), arg, MARTe::THREADS_DEFAULT_STACKSIZE, "WSClient"); } } /*---------------------------------------------------------------------------*/ /* HTTP upgrade */ /*---------------------------------------------------------------------------*/ bool WSServer::UpgradeHTTP(BasicTCPSocket *sock) { /* Read raw HTTP request until \r\n\r\n */ static const uint32 kMaxHdr = 4096u; char hdrBuf[4096]; uint32 totalRead = 0u; while (totalRead < kMaxHdr - 1u) { uint32 rem = kMaxHdr - 1u - totalRead; bool ok = sock->Read(hdrBuf + totalRead, rem, TimeoutType(3000u)); if (!ok || rem == 0u) { return false; } totalRead += rem; hdrBuf[totalRead] = '\0'; if (strstr(hdrBuf, "\r\n\r\n") != static_cast(0)) { break; } } /* Origin validation (CSWSH / CSRF defence, RFC 6455 §10.2). * If an Origin header is present, its host must match the Host header * (same-origin). Non-browser clients (no Origin) are allowed. */ const char *originHdr = FindSubstr(hdrBuf, "Origin:"); if (originHdr != static_cast(0)) { originHdr += 7; /* skip "Origin:" */ while (*originHdr == ' ') { originHdr++; } /* Extract the host part of Origin: "scheme://host[:port]" */ char originHost[256]; uint32 ohLen = 0u; const char *op = originHdr; /* Skip scheme:// */ const char *schemeEnd = strstr(op, "://"); if (schemeEnd != static_cast(0)) { op = schemeEnd + 3; } while (*op != '\r' && *op != '\n' && *op != '\0' && *op != '/' && ohLen < 255u) { originHost[ohLen++] = *op++; } originHost[ohLen] = '\0'; /* Extract Host header value */ const char *hostHdr = FindSubstr(hdrBuf, "Host:"); if (hostHdr != static_cast(0)) { hostHdr += 5; /* skip "Host:" */ while (*hostHdr == ' ') { hostHdr++; } char hostVal[256]; uint32 hvLen = 0u; while (*hostHdr != '\r' && *hostHdr != '\n' && *hostHdr != '\0' && hvLen < 255u) { hostVal[hvLen++] = *hostHdr++; } hostVal[hvLen] = '\0'; if (strcmp(originHost, hostVal) != 0) { /* Cross-origin — reject the upgrade */ const char *forbidden = "HTTP/1.1 403 Forbidden\r\n" "Content-Type: text/plain\r\n" "Connection: close\r\n" "\r\nOrigin not allowed\r\n"; uint32 forbLen = static_cast(strlen(forbidden)); (void) sock->Write(forbidden, forbLen); return false; } } } /* Find Sec-WebSocket-Key */ const char *keyHdr = FindSubstr(hdrBuf, "Sec-WebSocket-Key:"); if (keyHdr == static_cast(0)) { return false; } keyHdr += 18; /* skip "Sec-WebSocket-Key:" */ while (*keyHdr == ' ') { keyHdr++; } char key[64]; uint32 keyLen = 0u; while (keyHdr[keyLen] != '\r' && keyHdr[keyLen] != '\n' && keyHdr[keyLen] != '\0' && keyLen < 63u) { key[keyLen] = keyHdr[keyLen]; keyLen++; } key[keyLen] = '\0'; /* Compute Accept = Base64(SHA1(key + GUID)) */ static const char *kGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; char concat[128]; snprintf(concat, sizeof(concat), "%s%s", key, kGUID); uint8 digest[20]; SHA1(reinterpret_cast(concat), static_cast(strlen(concat)), digest); char accept[64]; Base64Encode(digest, 20u, accept); /* Send 101 response */ char resp[512]; snprintf(resp, sizeof(resp), "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n" "\r\n", accept); uint32 respLen = static_cast(strlen(resp)); return sock->Write(resp, respLen); } /*---------------------------------------------------------------------------*/ /* Client read loop */ /*---------------------------------------------------------------------------*/ void WSServer::ClientReadLoop(uint32 slotIdx) { WSClientSlot &slot = clients[slotIdx]; BasicTCPSocket *sock = slot.sock; /* Receive buffer: WS_MAX_RECV_PAYLOAD + max header (14: 2 + 8 ext-length + * 4 mask) + 1 spare byte for in-place NUL-termination of the payload. */ static const uint32 kRecvBuf = WS_MAX_RECV_PAYLOAD + 14u + 1u; uint8 *buf = new uint8[kRecvBuf]; uint32 filled = 0u; while (running && slot.active) { /* Read more bytes (with short timeout so we can check running) */ uint32 want = kRecvBuf - filled; if (want == 0u) { /* Buffer full — discard old frame (shouldn't happen with reasonable clients) */ filled = 0u; continue; } bool ok = sock->Read(reinterpret_cast(buf + filled), want, TimeoutType(500u)); if (!ok) { /* Timeout or error — check running and retry */ if (!running) { break; } if (want == kRecvBuf) { /* Zero bytes read — connection likely closed */ break; } continue; } filled += want; /* Parse as many complete frames as possible */ uint32 consumed = 0u; while (consumed < filled) { const uint8 *frameStart = buf + consumed; uint32 available = filled - consumed; WSFrameHeader hdr; if (!WSParseHeader(frameStart, available, hdr)) { break; /* incomplete header — need more data */ } if (hdr.payloadLen > static_cast(WS_MAX_RECV_PAYLOAD)) { /* Payload too large — close connection */ goto client_done; } uint32 plen = static_cast(hdr.payloadLen); uint32 totalFrameBytes = hdr.headerSize + plen; if (available < totalFrameBytes) { break; /* incomplete payload — need more data */ } /* Unmask payload */ uint8 *payload = const_cast(frameStart) + hdr.headerSize; if (hdr.masked) { WSUnmask(payload, plen, hdr.maskKey); } /* Handle opcode */ if (hdr.opcode == WS_OPCODE_CLOSE) { goto client_done; } else if (hdr.opcode == WS_OPCODE_PING) { /* Send pong */ (void) slot.writeMutex.FastLock(); SendFrame(slot, WS_OPCODE_PONG, payload, plen); slot.writeMutex.FastUnLock(); } else if (hdr.opcode == WS_OPCODE_TEXT) { if (callback != static_cast(0)) { /* NUL-terminate in place for the callback, but restore the * byte afterwards: when several client frames coalesce in * one TCP read, payload[plen] is the first header byte of * the NEXT frame. */ uint8 savedByte = payload[plen]; payload[plen] = '\0'; /* safe: buf has extra byte */ callback->OnWSCommand(reinterpret_cast(payload), plen, slotIdx); payload[plen] = savedByte; } } /* Binary frames from client are ignored (server only sends binary) */ consumed += totalFrameBytes; } /* Compact buffer: move unconsumed bytes to front */ if (consumed > 0u && consumed < filled) { memmove(buf, buf + consumed, filled - consumed); } filled = (consumed <= filled) ? (filled - consumed) : 0u; } client_done: delete[] buf; if (callback != static_cast(0)) { callback->OnWSClientDisconnected(); } FreeSlot(slotIdx); } /*---------------------------------------------------------------------------*/ /* Broadcast helpers */ /*---------------------------------------------------------------------------*/ void WSServer::BroadcastText(const char *json, uint32 len) { for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) { if (!clients[i].active) { continue; } (void) clients[i].writeMutex.FastLock(); if (clients[i].active) { (void) SendFrame(clients[i], WS_OPCODE_TEXT, reinterpret_cast(json), len); } clients[i].writeMutex.FastUnLock(); } } void WSServer::BroadcastBinary(const uint8 *buf, uint32 len) { for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) { if (!clients[i].active) { continue; } (void) clients[i].writeMutex.FastLock(); if (clients[i].active) { (void) SendFrame(clients[i], WS_OPCODE_BINARY, buf, len); } clients[i].writeMutex.FastUnLock(); } } void WSServer::SendText(uint32 slotIdx, const char *json, uint32 len) { if (slotIdx >= WS_MAX_CLIENTS) { return; } WSClientSlot &slot = clients[slotIdx]; if (!slot.active) { return; } (void) slot.writeMutex.FastLock(); if (slot.active) { (void) SendFrame(slot, WS_OPCODE_TEXT, reinterpret_cast(json), len); } slot.writeMutex.FastUnLock(); } uint32 WSServer::ClientCount() const { (void) clientsMutex.FastLock(); uint32 c = numClients; clientsMutex.FastUnLock(); return c; } /*---------------------------------------------------------------------------*/ /* Internal helpers */ /*---------------------------------------------------------------------------*/ bool WSServer::SendFrame(WSClientSlot &slot, uint8 opcode, const uint8 *payload, uint32 payloadLen) { if (!slot.active || (slot.sock == static_cast(0))) { return false; } uint8 hdrBuf[10]; uint32 hdrLen = WSEncodeHeader(hdrBuf, opcode, static_cast(payloadLen)); /* Write header */ uint32 sz = hdrLen; if (!slot.sock->Write(reinterpret_cast(hdrBuf), sz)) { return false; } /* Write payload */ sz = payloadLen; if (payloadLen > 0u) { if (!slot.sock->Write(reinterpret_cast(payload), sz)) { return false; } } return true; } uint32 WSServer::AllocSlot(BasicTCPSocket *sock) { (void) clientsMutex.FastLock(); uint32 idx = WS_MAX_CLIENTS; for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) { if (!clients[i].active) { clients[i].sock = sock; clients[i].active = true; clients[i].readTid = MARTe::InvalidThreadIdentifier; idx = i; numClients++; break; } } clientsMutex.FastUnLock(); return idx; } void WSServer::FreeSlot(uint32 idx) { if (idx >= WS_MAX_CLIENTS) { return; } /* HI-5: acquire writeMutex before modifying active/sock to prevent * use-after-free when BroadcastText/BroadcastBinary are iterating. */ (void) clients[idx].writeMutex.FastLock(); (void) clientsMutex.FastLock(); if (clients[idx].active) { clients[idx].active = false; if (clients[idx].sock != static_cast(0)) { clients[idx].sock->Close(); delete clients[idx].sock; clients[idx].sock = static_cast(0); } if (numClients > 0u) { numClients--; } } clientsMutex.FastUnLock(); clients[idx].writeMutex.FastUnLock(); } } /* namespace StreamHub */