diff --git a/cmake/SourceFileList.cmake b/cmake/SourceFileList.cmake index 4a0a772..b6ea5be 100644 --- a/cmake/SourceFileList.cmake +++ b/cmake/SourceFileList.cmake @@ -123,6 +123,7 @@ set(eoserv_ALL_SOURCE_FILES src/util/async.hpp src/util/rpn.cpp src/util/rpn.hpp + src/websocket.hpp src/util/secure_string.hpp src/util/semaphore.cpp src/util/semaphore.hpp diff --git a/config/server.ini b/config/server.ini index 79b1451..6468fab 100644 --- a/config/server.ini +++ b/config/server.ini @@ -13,6 +13,11 @@ Host = 0.0.0.0 # The port the server should listen on Port = 8078 +## WebSocketPort (number) +# The port the server should listen on for WebSocket connections +# 0 to disable WebSocket support +WebSocketPort = 0 + ## MaxConnections (number) # The maximum number of connections the server will allow at once MaxConnections = 300 diff --git a/src/eoclient.cpp b/src/eoclient.cpp index 69eb850..592c119 100644 --- a/src/eoclient.cpp +++ b/src/eoclient.cpp @@ -19,6 +19,7 @@ #include "console.hpp" #include "socket.hpp" #include "util.hpp" +#include "websocket.hpp" #include #include @@ -92,7 +93,111 @@ void EOClient::LogPacket(PacketFamily family, PacketAction action, size_t sz, co bool EOClient::NeedTick() { - return this->upload_fh; + return this->upload_fh + || (this->websocket_ && this->ws_payload_pos_ < this->ws_payload_buf_.size()); +} + +void EOClient::DoWsHandshake() +{ + // Drain recv_buffer into ws_buf_ to accumulate the HTTP upgrade request + while (this->recv_buffer_used > 0) + ws_buf_ += this->Recv(std::min(this->recv_buffer_used, std::size_t(256))); + + // Wait until we have the full HTTP request (ends with blank line) + if (ws_buf_.find("\r\n\r\n") == std::string::npos) + return; + + // Parse Sec-WebSocket-Key header + std::size_t key_pos = ws_buf_.find("Sec-WebSocket-Key:"); + if (key_pos == std::string::npos) + { + // Not a valid WebSocket upgrade request + this->Close(true); + return; + } + + key_pos += 18; // skip "Sec-WebSocket-Key:" + while (key_pos < ws_buf_.size() && ws_buf_[key_pos] == ' ') + ++key_pos; + + std::size_t key_end = ws_buf_.find_first_of("\r\n", key_pos); + if (key_end == std::string::npos) + { + this->Close(true); + return; + } + + std::string key = ws_buf_.substr(key_pos, key_end - key_pos); + + // Preserve any WS frame bytes that arrived after the HTTP headers + std::size_t header_end = ws_buf_.find("\r\n\r\n"); + ws_buf_ = ws_buf_.substr(header_end + 4); + + // Send HTTP 101 response (goes directly to socket send buffer) + Client::Send(websocket::build_handshake_response(key)); + + ws_handshake_done_ = true; +} + +void EOClient::DecodeWsFrames() +{ + if (!this->Connected()) + return; + + // Drain new raw bytes from recv_buffer into ws_buf_ + while (this->recv_buffer_used > 0) + ws_buf_ += this->Recv(std::min(this->recv_buffer_used, std::size_t(4096))); + + // Parse complete frames from ws_buf_ + std::size_t pos = 0; + while (pos < ws_buf_.size()) + { + std::string payload; + uint8_t opcode = 0; + std::size_t consumed = 0; + + if (!websocket::decode_frame(ws_buf_.c_str() + pos, ws_buf_.size() - pos, payload, opcode, consumed)) + break; // incomplete frame — wait for more data + + if (opcode == websocket::WS_OPCODE_CLOSE) + { + // RFC 6455: echo the close frame, then close + Client::Send(websocket::wrap_close_frame()); + ws_buf_.clear(); + this->Close(true); + return; + } + else if (opcode == websocket::WS_OPCODE_BINARY || opcode == websocket::WS_OPCODE_CONTINUATION) + { + ws_payload_buf_ += payload; + } + // Silently discard ping / pong / text frames + + pos += consumed; + } + + if (pos > 0) + ws_buf_.erase(0, pos); +} + +std::string EOClient::WsRecv(std::size_t length) +{ + // Periodically compact the decoded payload buffer + if (ws_payload_pos_ > 4096) + { + ws_payload_buf_.erase(0, ws_payload_pos_); + ws_payload_pos_ = 0; + } + + std::size_t available = ws_payload_buf_.size() - ws_payload_pos_; + length = std::min(length, available); + + if (length == 0) + return std::string(); + + std::string ret = ws_payload_buf_.substr(ws_payload_pos_, length); + ws_payload_pos_ += length; + return ret; } void EOClient::Tick() @@ -104,6 +209,7 @@ void EOClient::Tick() if (this->upload_fh) { // Send more of the file instead of doing other tasks + // (WebSocket clients use the single-packet path in Upload(), so this is TCP only) std::size_t upload_available = std::min(this->upload_size - this->upload_pos, Client::SendBufferRemaining()); if (upload_available != 0) @@ -149,7 +255,21 @@ void EOClient::Tick() } else { - data = this->Recv((this->packet_state == EOClient::ReadData) ? this->length : 1); + if (this->websocket_) + { + if (!this->ws_handshake_done_) + { + this->DoWsHandshake(); + return; + } + + this->DecodeWsFrames(); + data = this->WsRecv((this->packet_state == EOClient::ReadData) ? this->length : 1); + } + else + { + data = this->Recv((this->packet_state == EOClient::ReadData) ? this->length : 1); + } while (data.length() > 0 && !done) { @@ -356,6 +476,47 @@ bool EOClient::Upload(FileType type, const std::string &filename, InitReply init if (this->upload_fh) throw std::runtime_error("Already uploading file"); + // For WebSocket clients, embed the entire file in one packet (reoserv-compatible). + // Browsers receive each WS message as a discrete blob, so raw streaming bytes sent + // as separate WS messages would not be recognizable as file data by the web client. + if (this->websocket_) + { + FILE* fh = std::fopen(filename.c_str(), "rb"); + if (!fh) + return false; + + if (std::fseek(fh, 0, SEEK_END) != 0) + { + std::fclose(fh); + return false; + } + + std::size_t file_size = static_cast(std::ftell(fh)); + std::fseek(fh, 0, SEEK_SET); + + std::string file_data(file_size, '\0'); + std::fread(&file_data[0], 1, file_size, fh); + std::fclose(fh); + + // Apply PK patch bytes directly into the raw file data before sending + if (type == FILE_MAP && this->server()->world->config["GlobalPK"] && !this->server()->world->PKExcept(player->character->mapid)) + { + if (file_size > 0x03) file_data[0x03] = static_cast(0xFF); + if (file_size > 0x04) file_data[0x04] = static_cast(0x01); + if (file_size > 0x1F) file_data[0x1F] = static_cast(0x04); + } + + PacketBuilder builder(PACKET_F_INIT, PACKET_A_INIT, 1 + (type != FILE_MAP ? 1 : 0) + file_size); + builder.AddChar(init_reply); + if (type != FILE_MAP) + builder.AddChar(1); + builder.AddString(file_data); + + LogPacket(PACKET_F_INIT, PACKET_A_INIT, builder.Length(), "UPLD"); + Client::Send(websocket::wrap_frame(this->processor.Encode(builder))); + return true; + } + this->upload_fh = std::fopen(filename.c_str(), "rb"); if (!this->upload_fh) @@ -389,7 +550,7 @@ bool EOClient::Upload(FileType type, const std::string &filename, InitReply init swap(this->send_buffer_ppos, this->send_buffer2_ppos); swap(this->send_buffer_used, this->send_buffer2_used); - // Build the file upload header packet + // Build the file upload header packet (TCP: size announced, data streamed separately) PacketBuilder builder(PACKET_F_INIT, PACKET_A_INIT, 2); builder.AddChar(init_reply); @@ -399,7 +560,6 @@ bool EOClient::Upload(FileType type, const std::string &filename, InitReply init builder.AddSize(this->upload_size); LogPacket(PACKET_F_INIT, PACKET_A_INIT, builder.Length(), "UPLD"); - Client::Send(builder); return true; @@ -415,6 +575,9 @@ void EOClient::Send(const PacketBuilder &builder) std::string data = this->processor.Encode(builder); + if (this->websocket_) + data = websocket::wrap_frame(data); + if (this->upload_fh) { // Stick any incoming data in to our temporary buffer diff --git a/src/eoclient.hpp b/src/eoclient.hpp index 88a1275..d6a3d44 100644 --- a/src/eoclient.hpp +++ b/src/eoclient.hpp @@ -100,6 +100,17 @@ class EOClient : public Client std::mutex send_mutex; + // WebSocket state + bool websocket_ = false; + bool ws_handshake_done_ = false; + std::string ws_buf_; // HTTP upgrade accumulation / partial WS frame bytes + std::string ws_payload_buf_; // Decoded WS payload ready for EO packet processing + std::size_t ws_payload_pos_ = 0; + + void DoWsHandshake(); + void DecodeWsFrames(); + std::string WsRecv(std::size_t length); + public: EOServer *server() { return static_cast(Client::server); }; int version; @@ -135,6 +146,9 @@ class EOClient : public Client void Tick(); + void SetWebSocket(bool ws) { this->websocket_ = ws; } + bool IsWebSocket() const { return this->websocket_; } + void InitNewSequence(); void PingNewSequence(); void PongNewSequence(); diff --git a/src/eoserv_config.cpp b/src/eoserv_config.cpp index 9cd1766..1569e99 100644 --- a/src/eoserv_config.cpp +++ b/src/eoserv_config.cpp @@ -34,6 +34,7 @@ void eoserv_config_validate_config(Config& config) eoserv_config_default(config, "LogConnection" , 0); eoserv_config_default(config, "Host" , "0.0.0.0"); eoserv_config_default(config, "Port" , 8078); + eoserv_config_default(config, "WebSocketPort" , 0); eoserv_config_default(config, "MaxConnections" , 300); eoserv_config_default(config, "ListenBacklog" , 50); eoserv_config_default(config, "MaxPlayers" , 200); diff --git a/src/eoserver.cpp b/src/eoserver.cpp index e7cd582..590c178 100644 --- a/src/eoserver.cpp +++ b/src/eoserver.cpp @@ -16,10 +16,12 @@ #include "console.hpp" #include "socket.hpp" +#include "socket_impl.hpp" #include "util.hpp" #include #include +#include #include #include #include @@ -192,6 +194,47 @@ void EOServer::Initialize(std::shared_ptr databaseFactory, cons this->start = Timer::GetTime(); + // Set up optional WebSocket listener + int ws_port = int(this->world->config["WebSocketPort"]); + if (ws_port > 0) + { + this->ws_listen_sock_ = socket(AF_INET, SOCK_STREAM, 0); + + if (this->ws_listen_sock_ != INVALID_SOCKET) + { + const int yes = 1; +#ifdef WIN32 + setsockopt(this->ws_listen_sock_, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&yes), sizeof(yes)); +#else + setsockopt(this->ws_listen_sock_, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(this->ws_listen_sock_, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); +#endif + + sockaddr_in sin; + std::memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(this->address); + sin.sin_port = htons(static_cast(ws_port)); + + if (bind(this->ws_listen_sock_, reinterpret_cast(&sin), sizeof(sin)) == SOCKET_ERROR + || listen(this->ws_listen_sock_, int(this->world->config["ListenBacklog"])) == SOCKET_ERROR) + { + Console::Err("Failed to bind WebSocket listener on port %i", ws_port); +#ifdef WIN32 + closesocket(this->ws_listen_sock_); +#else + close(this->ws_listen_sock_); +#endif + this->ws_listen_sock_ = INVALID_SOCKET; + } + else + { + this->ws_enabled_ = true; + } + } + } + this->UpdateConfig(); } @@ -200,6 +243,103 @@ Client *EOServer::ClientFactory(const Socket &sock) return new EOClient(sock, this); } +void EOServer::PollWebSocket() +{ + sockaddr_in sin; + socklen_t addrsize = sizeof(sockaddr_in); + +#ifdef WIN32 + unsigned long nonblocking = 1; + ioctlsocket(this->ws_listen_sock_, FIONBIO, &nonblocking); +#else + fcntl(this->ws_listen_sock_, F_SETFL, FNONBLOCK|FASYNC); +#endif + + SOCKET newsock = accept(this->ws_listen_sock_, reinterpret_cast(&sin), &addrsize); + +#ifdef WIN32 + nonblocking = 0; + ioctlsocket(this->ws_listen_sock_, FIONBIO, &nonblocking); +#else + fcntl(this->ws_listen_sock_, F_SETFL, 0); +#endif + + if (newsock == INVALID_SOCKET) + return; + + // Apply the same rate-limiting and connection limit checks as Tick() + double now = Timer::GetTime(); + IPAddress remote_addr(ntohl(sin.sin_addr.s_addr)); + int ip_connections = 0; + bool throttle = false; + + const double reconnect_limit = int(this->world->config["IPReconnectLimit"]); + const int max_per_ip = int(this->world->config["MaxConnectionsPerIP"]); + const int log_connection = static_cast(int(this->world->config["LogConnection"])); + + // Check if server is at capacity + if (this->clients.size() >= this->maxconn) + { +#ifdef WIN32 + closesocket(newsock); +#else + close(newsock); +#endif + return; + } + + UTIL_IFOREACH(connection_log, connection) + { + double last_connection_time = connection->second.last_connection_time; + double last_rejection_time = connection->second.last_rejection_time; + + if (last_connection_time + reconnect_limit < now + && last_rejection_time + 30.0 < now) + { + connection = connection_log.erase(connection); + + if (connection == connection_log.end()) + break; + + continue; + } + + if (connection->first == remote_addr + && last_connection_time + reconnect_limit >= now) + { + throttle = true; + } + } + + UTIL_FOREACH(this->clients, client) + { + if (client->GetRemoteAddr() == remote_addr) + ++ip_connections; + } + + EOClient *newclient = new EOClient(Socket(newsock, sin), this); + newclient->SetRecvBuffer(this->recv_buffer_max); + newclient->SetSendBuffer(this->send_buffer_max); + newclient->SetWebSocket(true); + this->clients.push_back(newclient); + + if (throttle) + { + this->RecordClientRejection(remote_addr, "reconnecting too fast"); + newclient->Close(true); + } + else if (max_per_ip != 0 && ip_connections > max_per_ip) + { + this->RecordClientRejection(remote_addr, "too many connections from this address"); + newclient->Close(true); + } + else if (log_connection == LogConnection::LogAll || (log_connection == LogConnection::FilterPrivate && !remote_addr.IsPrivate())) + { + connection_log[remote_addr].last_connection_time = Timer::GetTime(); + Console::Out("New WebSocket connection from %s (%i/%i connections)", std::string(remote_addr).c_str(), this->Connections(), this->MaxConnections()); + } +} + void EOServer::Tick() { std::vector *active_clients = 0; @@ -296,6 +436,9 @@ void EOServer::Tick() this->BuryTheDead(); this->world->timer.Tick(); + + if (this->ws_enabled_) + this->PollWebSocket(); } void EOServer::RecordClientRejection(const IPAddress& ip, const char* reason) @@ -343,5 +486,15 @@ EOServer::~EOServer() delete this->sln; delete this->world; + if (this->ws_enabled_ && this->ws_listen_sock_ != INVALID_SOCKET) + { +#ifdef WIN32 + closesocket(this->ws_listen_sock_); +#else + close(this->ws_listen_sock_); +#endif + this->ws_listen_sock_ = INVALID_SOCKET; + } + Close(); } diff --git a/src/eoserver.hpp b/src/eoserver.hpp index f39330e..775aeae 100644 --- a/src/eoserver.hpp +++ b/src/eoserver.hpp @@ -17,6 +17,7 @@ #include "fwd/world.hpp" #include "socket.hpp" +#include "socket_impl.hpp" #include #include @@ -43,6 +44,12 @@ class EOServer : public Server TimeEvent* ping_timer = nullptr; + // WebSocket listener (separate port, optional) + SOCKET ws_listen_sock_ = INVALID_SOCKET; + bool ws_enabled_ = false; + + void PollWebSocket(); + protected: virtual Client *ClientFactory(const Socket &); diff --git a/src/loginmanager.hpp b/src/loginmanager.hpp index 78eaac2..6b4eac3 100644 --- a/src/loginmanager.hpp +++ b/src/loginmanager.hpp @@ -14,7 +14,7 @@ #include #include "hash.hpp" -#include "fwd/config.hpp" +#include "config.hpp" #include "fwd/database.hpp" #include "fwd/player.hpp" #include "fwd/world.hpp" @@ -36,7 +36,7 @@ class LoginManager void UpdatePasswordVersionInBackground(AccountCredentials&& accountCredentials); - bool LoginBusy() const { return this->_processCount >= static_cast(this->_config["LoginQueueSize"]); }; + bool LoginBusy() const { return this->_processCount >= static_cast(this->_config.at("LoginQueueSize")); }; private: std::shared_ptr _databaseFactory; diff --git a/src/main.cpp b/src/main.cpp index 2e99edc..e81839c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -374,6 +374,9 @@ Julian Smythe ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═ server->Listen(int(config["MaxConnections"]), int(config["ListenBacklog"])); Console::Out("Listening on %s:%i (0/%i connections)", std::string(config["Host"]).c_str(), int(config["Port"]), int(config["MaxConnections"])); + if (int(config["WebSocketPort"]) > 0) + Console::Out("Listening for WebSocket connections on %s:%i", std::string(config["Host"]).c_str(), int(config["WebSocketPort"])); + bool tables_exist = false; bool tried_install = false; diff --git a/src/socket.cpp b/src/socket.cpp index 291b54c..d65396f 100644 --- a/src/socket.cpp +++ b/src/socket.cpp @@ -504,7 +504,9 @@ bool Client::Select(double timeout) { fd_set read_fds, write_fds, except_fds; long tsecs = long(timeout); - timeval timeout_val = {tsecs, long((timeout - double(tsecs))*1000000)}; + timeval timeout_val; + timeout_val.tv_sec = tsecs; + timeout_val.tv_usec = long((timeout - double(tsecs))*1000000); FD_ZERO(&read_fds); FD_ZERO(&write_fds); @@ -860,7 +862,9 @@ std::vector *Server::Select(double timeout) std::vector *Server::Select(double timeout) { long tsecs = long(timeout); - timeval timeout_val = {tsecs, long((timeout - double(tsecs))*1000000)}; + timeval timeout_val; + timeout_val.tv_sec = tsecs; + timeout_val.tv_usec = long((timeout - double(tsecs))*1000000); static std::vector selected; SOCKET nfds = this->impl->sock; int result; diff --git a/src/websocket.hpp b/src/websocket.hpp new file mode 100644 index 0000000..dbfb7c9 --- /dev/null +++ b/src/websocket.hpp @@ -0,0 +1,258 @@ + +/* $Id$ + * EOSERV is released under the zlib license. + * See LICENSE.txt for more info. + */ + +#ifndef WEBSOCKET_HPP_INCLUDED +#define WEBSOCKET_HPP_INCLUDED + +#include +#include +#include + +/** + * Utilities for the WebSocket protocol (RFC 6455). + * Provides SHA-1 (for handshake key), base64 encoding, and frame encode/decode. + */ +namespace websocket +{ + +namespace detail +{ + +inline uint32_t rotl32(uint32_t n, unsigned int c) +{ + return (n << c) | (n >> (32 - c)); +} + +/** + * Compute SHA-1 digest of input. + * Returns a 20-byte binary string. + */ +inline std::string sha1(const std::string &input) +{ + uint32_t h0 = 0x67452301u; + uint32_t h1 = 0xEFCDAB89u; + uint32_t h2 = 0x98BADCFEu; + uint32_t h3 = 0x10325476u; + uint32_t h4 = 0xC3D2E1F0u; + + // Pre-process: pad message + std::string msg = input; + uint64_t orig_bits = static_cast(input.size()) * 8; + + msg += '\x80'; + while (msg.size() % 64 != 56) + msg += '\x00'; + + // Append original bit length as big-endian 64-bit integer + for (int i = 7; i >= 0; --i) + msg += static_cast((orig_bits >> (i * 8)) & 0xFF); + + // Process each 512-bit (64-byte) chunk + for (std::size_t i = 0; i < msg.size(); i += 64) + { + uint32_t w[80]; + + for (int j = 0; j < 16; ++j) + { + w[j] = (static_cast(msg[i + j*4]) << 24) + | (static_cast(msg[i + j*4 + 1]) << 16) + | (static_cast(msg[i + j*4 + 2]) << 8) + | static_cast(msg[i + j*4 + 3]); + } + + for (int j = 16; j < 80; ++j) + w[j] = rotl32(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + + uint32_t a = h0, b = h1, c = h2, d = h3, e = h4; + + for (int j = 0; j < 80; ++j) + { + uint32_t f, k; + + if (j < 20) { f = (b & c) | (~b & d); k = 0x5A827999u; } + else if (j < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1u; } + else if (j < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDCu; } + else { f = b ^ c ^ d; k = 0xCA62C1D6u; } + + uint32_t temp = rotl32(a, 5) + f + e + k + w[j]; + e = d; d = c; c = rotl32(b, 30); b = a; a = temp; + } + + h0 += a; h1 += b; h2 += c; h3 += d; h4 += e; + } + + // Produce 20-byte digest + std::string digest(20, '\0'); + for (int i = 0; i < 4; ++i) { + digest[i] = static_cast((h0 >> (24 - i*8)) & 0xFF); + digest[4 + i] = static_cast((h1 >> (24 - i*8)) & 0xFF); + digest[8 + i] = static_cast((h2 >> (24 - i*8)) & 0xFF); + digest[12 + i] = static_cast((h3 >> (24 - i*8)) & 0xFF); + digest[16 + i] = static_cast((h4 >> (24 - i*8)) & 0xFF); + } + + return digest; +} + +/** + * Base64-encode binary data. + */ +inline std::string base64_encode(const std::string &input) +{ + static const char *table = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string out; + out.reserve(((input.size() + 2) / 3) * 4); + + for (std::size_t i = 0; i < input.size(); i += 3) + { + uint32_t b = static_cast(input[i]) << 16; + + if (i + 1 < input.size()) b |= static_cast(input[i + 1]) << 8; + if (i + 2 < input.size()) b |= static_cast(input[i + 2]); + + out += table[(b >> 18) & 63]; + out += table[(b >> 12) & 63]; + out += (i + 1 < input.size()) ? table[(b >> 6) & 63] : '='; + out += (i + 2 < input.size()) ? table[ b & 63] : '='; + } + + return out; +} + +} // namespace detail + +/** + * Build the HTTP 101 upgrade response for a WebSocket handshake. + * @param key The value of the client's Sec-WebSocket-Key header. + */ +inline std::string build_handshake_response(const std::string &key) +{ + static const std::string MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + std::string accept = detail::base64_encode(detail::sha1(key + MAGIC)); + + return "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + accept + "\r\n\r\n"; +} + +/** + * Build a WebSocket close frame (server->client, unmasked, no payload). + */ +inline std::string wrap_close_frame() +{ + return std::string("\x88\x00", 2); // FIN=1, opcode=8 (close), no mask, 0 payload +} + +/** + * Wrap payload in a binary WebSocket frame (server->client, unmasked). + * Opcode 0x2 (binary), FIN=1. + */ +inline std::string wrap_frame(const std::string &payload) +{ + std::string frame; + std::size_t len = payload.size(); + + frame += '\x82'; // FIN=1, RSV=0, opcode=2 (binary) + + if (len <= 125) + { + frame += static_cast(len); + } + else if (len <= 65535) + { + frame += '\x7E'; // extended 16-bit length + frame += static_cast((len >> 8) & 0xFF); + frame += static_cast( len & 0xFF); + } + else + { + frame += '\x7F'; // extended 64-bit length + for (int i = 7; i >= 0; --i) + frame += static_cast((len >> (i * 8)) & 0xFF); + } + + frame += payload; + return frame; +} + +// Opcodes +static const uint8_t WS_OPCODE_CONTINUATION = 0x0; +static const uint8_t WS_OPCODE_TEXT = 0x1; +static const uint8_t WS_OPCODE_BINARY = 0x2; +static const uint8_t WS_OPCODE_CLOSE = 0x8; +static const uint8_t WS_OPCODE_PING = 0x9; +static const uint8_t WS_OPCODE_PONG = 0xA; + +/** + * Attempt to decode one WebSocket frame from buf[0..len-1]. + * @param buf Raw bytes (client-to-server, expected to be masked). + * @param len Number of bytes available. + * @param out_payload Decoded (unmasked) payload on success. + * @param out_opcode Frame opcode. + * @param out_consumed Number of bytes consumed from buf on success. + * @return true if a complete frame was decoded, false if more data is needed. + */ +inline bool decode_frame(const char *buf, std::size_t len, + std::string &out_payload, uint8_t &out_opcode, + std::size_t &out_consumed) +{ + if (len < 2) + return false; + + uint8_t b0 = static_cast(buf[0]); + uint8_t b1 = static_cast(buf[1]); + + out_opcode = b0 & 0x0F; + bool masked = (b1 & 0x80) != 0; + uint64_t payload_len = b1 & 0x7F; + + std::size_t header_len = 2; + + if (payload_len == 126) + { + if (len < 4) return false; + payload_len = (static_cast(static_cast(buf[2])) << 8) + | static_cast(buf[3]); + header_len = 4; + } + else if (payload_len == 127) + { + if (len < 10) return false; + payload_len = 0; + for (int i = 0; i < 8; ++i) + payload_len = (payload_len << 8) | static_cast(buf[2 + i]); + header_len = 10; + } + + if (masked) + header_len += 4; + + if (len < header_len + static_cast(payload_len)) + return false; + + const char *mask_key = nullptr; + if (masked) + mask_key = buf + header_len - 4; + + out_payload.resize(static_cast(payload_len)); + for (std::size_t i = 0; i < static_cast(payload_len); ++i) + { + uint8_t byte = static_cast(buf[header_len + i]); + if (masked) + byte ^= static_cast(mask_key[i % 4]); + out_payload[i] = static_cast(byte); + } + + out_consumed = header_len + static_cast(payload_len); + return true; +} + +} // namespace websocket + +#endif // WEBSOCKET_HPP_INCLUDED