448 lines
15 KiB
C++
448 lines
15 KiB
C++
/**
|
|
* @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 <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
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<AcceptThreadArg *>(
|
|
const_cast<void *>(arg))->srv;
|
|
delete reinterpret_cast<const AcceptThreadArg *>(arg);
|
|
srv->AcceptLoop();
|
|
}
|
|
|
|
static void ClientReadEntry(const void *arg) {
|
|
const ClientThreadArg *a = reinterpret_cast<const ClientThreadArg *>(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<WSCommandCallback *>(0)),
|
|
running(false),
|
|
acceptTid(MARTe::InvalidThreadIdentifier) {
|
|
|
|
for (uint32 i = 0u; i < WS_MAX_CLIENTS; i++) {
|
|
clients[i].sock = static_cast<BasicTCPSocket *>(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<MARTe::int32>(WS_MAX_CLIENTS))) {
|
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::FatalError,
|
|
"WSServer: Failed to listen on port %u.", static_cast<uint32>(port));
|
|
running = false;
|
|
return false;
|
|
}
|
|
|
|
AcceptThreadArg *arg = new AcceptThreadArg();
|
|
arg->srv = this;
|
|
acceptTid = MARTe::Threads::BeginThread(
|
|
reinterpret_cast<MARTe::ThreadFunctionType>(AcceptThreadEntry),
|
|
arg, MARTe::THREADS_DEFAULT_STACKSIZE, "WSAccept");
|
|
|
|
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
|
|
"WSServer: Listening on port %u.", static_cast<uint32>(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<BasicTCPSocket *>(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<BasicTCPSocket *>(0)) {
|
|
delete clients[i].sock;
|
|
clients[i].sock = static_cast<BasicTCPSocket *>(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<BasicTCPSocket *>(0));
|
|
if (client == static_cast<BasicTCPSocket *>(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<uint32>(strlen(full));
|
|
(void) client->Write(full, l);
|
|
client->Close();
|
|
delete client;
|
|
continue;
|
|
}
|
|
|
|
/* Notify StreamHub */
|
|
if (callback != static_cast<WSCommandCallback *>(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<MARTe::ThreadFunctionType>(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<char *>(0)) { break; }
|
|
}
|
|
|
|
/* Find Sec-WebSocket-Key */
|
|
const char *keyHdr = FindSubstr(hdrBuf, "Sec-WebSocket-Key:");
|
|
if (keyHdr == static_cast<const char *>(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<const uint8 *>(concat),
|
|
static_cast<uint32>(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<uint32>(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 (grows as needed by simple state machine) */
|
|
static const uint32 kRecvBuf = WS_MAX_RECV_PAYLOAD + 14u;
|
|
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<char *>(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<uint64>(WS_MAX_RECV_PAYLOAD)) {
|
|
/* Payload too large — close connection */
|
|
goto client_done;
|
|
}
|
|
uint32 plen = static_cast<uint32>(hdr.payloadLen);
|
|
uint32 totalFrameBytes = hdr.headerSize + plen;
|
|
if (available < totalFrameBytes) {
|
|
break; /* incomplete payload — need more data */
|
|
}
|
|
|
|
/* Unmask payload */
|
|
uint8 *payload = const_cast<uint8 *>(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<WSCommandCallback *>(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<const char *>(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<WSCommandCallback *>(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<const uint8 *>(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<const uint8 *>(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<BasicTCPSocket *>(0))) {
|
|
return false;
|
|
}
|
|
uint8 hdrBuf[10];
|
|
uint32 hdrLen = WSEncodeHeader(hdrBuf, opcode,
|
|
static_cast<uint64>(payloadLen));
|
|
|
|
/* Write header */
|
|
uint32 sz = hdrLen;
|
|
if (!slot.sock->Write(reinterpret_cast<const char *>(hdrBuf), sz)) {
|
|
return false;
|
|
}
|
|
/* Write payload */
|
|
sz = payloadLen;
|
|
if (payloadLen > 0u) {
|
|
if (!slot.sock->Write(reinterpret_cast<const char *>(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; }
|
|
(void) clientsMutex.FastLock();
|
|
if (clients[idx].active) {
|
|
clients[idx].active = false;
|
|
if (clients[idx].sock != static_cast<BasicTCPSocket *>(0)) {
|
|
clients[idx].sock->Close();
|
|
delete clients[idx].sock;
|
|
clients[idx].sock = static_cast<BasicTCPSocket *>(0);
|
|
}
|
|
if (numClients > 0u) { numClients--; }
|
|
}
|
|
clientsMutex.FastUnLock();
|
|
}
|
|
|
|
} /* namespace StreamHub */
|