18#include "../../core/wire.hpp"
20namespace morph::net::detail {
23enum class WsOpcode : std::uint8_t {
35 WsOpcode opcode{WsOpcode::kText};
40namespace ws_frame_impl {
55inline std::array<std::uint8_t, 4> randomMaskKey() {
56 static thread_local std::random_device entropy;
57 std::random_device::result_type
const bits = entropy();
58 return std::array<std::uint8_t, 4>{
static_cast<std::uint8_t
>(bits >> 24U),
static_cast<std::uint8_t
>(bits >> 16U),
59 static_cast<std::uint8_t
>(bits >> 8U),
static_cast<std::uint8_t
>(bits)};
70inline bool isValidCloseCode(std::uint16_t code) {
71 if (code >= 1000 && code <= 1003) {
74 if (code >= 1007 && code <= 1014) {
77 return code >= 3000 && code <= 4999;
88 bool feed(std::string_view data) {
89 while (!data.empty()) {
90 if (_remaining == 0) {
91 data = skipAscii(data);
96 auto const currentByte =
static_cast<unsigned char>(data.front());
97 data.remove_prefix(1);
98 if (!(_remaining == 0 ? startSequence(currentByte) : continueSequence(currentByte))) {
107 [[nodiscard]]
bool complete()
const {
return _remaining == 0; }
115 static std::string_view skipAscii(std::string_view data) {
116 constexpr std::uint64_t kHighBits = 0x8080808080808080ULL;
117 std::uint64_t word = 0;
118 while (data.size() >=
sizeof word) {
119 std::memcpy(&word, data.data(),
sizeof word);
120 if ((word & kHighBits) != 0) {
123 data.remove_prefix(
sizeof word);
125 while (!data.empty() &&
static_cast<unsigned char>(data.front()) < 0x80U) {
126 data.remove_prefix(1);
132 bool startSequence(
unsigned char currentByte) {
133 if (currentByte < 0x80) {
136 if ((currentByte & 0xE0U) == 0xC0U) {
137 if (currentByte < 0xC2U) {
141 _codepoint = currentByte & 0x1FU;
142 _minCodepoint = 0x80;
145 if ((currentByte & 0xF0U) == 0xE0U) {
147 _codepoint = currentByte & 0x0FU;
148 _minCodepoint = 0x800;
151 if ((currentByte & 0xF8U) == 0xF0U) {
152 if (currentByte > 0xF4U) {
156 _codepoint = currentByte & 0x07U;
157 _minCodepoint = 0x10000;
164 bool continueSequence(
unsigned char currentByte) {
165 if ((currentByte & 0xC0U) != 0x80U) {
168 _codepoint = (_codepoint << 6) | (currentByte & 0x3FU);
169 if (--_remaining != 0) {
172 return _codepoint >= _minCodepoint && _codepoint <= 0x10FFFFU &&
173 (_codepoint < 0xD800U || _codepoint > 0xDFFFU);
176 unsigned _remaining{0};
177 std::uint32_t _codepoint{0};
178 std::uint32_t _minCodepoint{0};
188inline std::string encodeWsFrame(WsOpcode opcode, std::string_view payload,
bool mask) {
190 out.push_back(
static_cast<char>(0x80u |
static_cast<std::uint8_t
>(opcode)));
191 std::uint64_t
const len = payload.size();
192 std::uint8_t
const maskBit = mask ? 0x80u : 0x00u;
194 out.push_back(
static_cast<char>(maskBit |
static_cast<std::uint8_t
>(len)));
195 }
else if (len <= 0xFFFFu) {
196 out.push_back(
static_cast<char>(maskBit | 126u));
197 out.push_back(
static_cast<char>((len >> 8) & 0xFFu));
198 out.push_back(
static_cast<char>(len & 0xFFu));
200 out.push_back(
static_cast<char>(maskBit | 127u));
201 for (
int shift = 56; shift >= 0; shift -= 8) {
202 out.push_back(
static_cast<char>((len >> shift) & 0xFFu));
206 auto key = ws_frame_impl::randomMaskKey();
208 out.push_back(
static_cast<char>(b));
210 for (std::size_t i = 0; i < payload.size(); ++i) {
211 out.push_back(
static_cast<char>(
static_cast<std::uint8_t
>(payload[i]) ^ key[i % key.size()]));
234 explicit WsFrameReader(
bool expectMasked) : _expectMasked(expectMasked) {}
238 void feed(std::string_view data) { _buf.append(data); }
262 std::optional<WsFrame> tryExtractFrame() {
264 auto frame = tryExtractRawFrame();
268 if (isControlOpcode(frame->opcode)) {
269 return handleControlFrame(*frame);
271 if (frame->opcode == WsOpcode::kContinuation) {
272 if (
auto completed = handleContinuationFrame(*frame)) {
277 if (
auto completed = handleDataFrame(*frame)) {
296 static WsFrame handleControlFrame(RawFrame& frame) {
297 return WsFrame{.opcode = frame.opcode, .payload = std::move(frame.payload)};
302 std::optional<WsFrame> handleContinuationFrame(RawFrame& frame) {
304 throw std::runtime_error(
"WsFrameReader: continuation frame with no message in progress");
306 feedAssemblyTextOrThrow(frame.payload);
307 appendFragment(frame.payload);
311 if (assemblyIsText() && !_textUtf8.complete()) {
313 throw std::runtime_error(
"WsFrameReader: text message ends mid-UTF-8-sequence");
315 WsFrame completed{.opcode = _assemblyOpcode, .payload = std::move(_assembly)};
323 std::optional<WsFrame> handleDataFrame(RawFrame& frame) {
325 throw std::runtime_error(
"WsFrameReader: new data frame while a fragmented message is in progress");
327 if (frame.opcode == WsOpcode::kText && frame.fin) {
328 ws_frame_impl::Utf8Validator validator;
329 if (!validator.feed(frame.payload) || !validator.complete()) {
330 throw std::runtime_error(
"WsFrameReader: text message contains invalid UTF-8");
334 return WsFrame{.opcode = frame.opcode,
335 .payload = std::move(frame.payload)};
338 _assemblyOpcode = frame.opcode;
339 _textUtf8 = ws_frame_impl::Utf8Validator{};
340 feedAssemblyTextOrThrow(frame.payload);
342 appendFragment(frame.payload);
350 [[nodiscard]]
bool assemblyIsText()
const {
return _assemblyOpcode == WsOpcode::kText; }
356 void feedAssemblyTextOrThrow(
const std::string& payload) {
357 if (assemblyIsText() && !_textUtf8.feed(payload)) {
359 throw std::runtime_error(
"WsFrameReader: text message contains invalid UTF-8");
363 void resetAssembly() {
365 _textUtf8 = ws_frame_impl::Utf8Validator{};
367 _assembly.shrink_to_fit();
370 void appendFragment(
const std::string& payload) {
374 if (_assembly.size() + payload.size() > ::morph::wire::kMaxEnvelopeBytes) {
376 throw std::runtime_error(
"WsFrameReader: reassembled message exceeds kMaxEnvelopeBytes");
378 _assembly += payload;
382 static WsOpcode validateFirstByte(std::uint8_t byte0) {
383 if ((byte0 & 0x70U) != 0) {
384 throw std::runtime_error(
"WsFrameReader: RSV bit set with no extension negotiated");
386 auto const opcode =
static_cast<WsOpcode
>(
static_cast<std::uint8_t
>(byte0 & 0x0FU));
388 case WsOpcode::kContinuation:
389 case WsOpcode::kText:
390 case WsOpcode::kBinary:
391 case WsOpcode::kClose:
392 case WsOpcode::kPing:
393 case WsOpcode::kPong:
396 throw std::runtime_error(
"WsFrameReader: reserved opcode");
400 void validateMaskDirection(
bool masked)
const {
401 if (masked != _expectMasked) {
402 throw std::runtime_error(_expectMasked ?
"WsFrameReader: unmasked frame from a client"
403 :
"WsFrameReader: masked frame from a server");
407 static bool isControlOpcode(WsOpcode opcode) {
return (
static_cast<std::uint8_t
>(opcode) & 0x08U) != 0; }
409 static void validateControlFraming(
bool isControl,
bool fin, std::uint8_t lenMarker) {
414 throw std::runtime_error(
"WsFrameReader: control frames must not be fragmented");
416 if (lenMarker > 125) {
417 throw std::runtime_error(
"WsFrameReader: control frame payload exceeds 125 bytes");
422 struct LengthDecode {
423 std::uint64_t payloadLen;
424 std::size_t headerLen;
429 [[nodiscard]] std::optional<LengthDecode> decodeLength(std::uint8_t lenMarker)
const {
430 std::uint64_t payloadLen = lenMarker;
431 std::size_t headerLen = 2;
432 if (payloadLen == 126) {
433 if (_buf.size() < 4) {
436 payloadLen = (
static_cast<std::uint64_t
>(
static_cast<std::uint8_t
>(_buf[2])) << 8) |
437 static_cast<std::uint64_t
>(
static_cast<std::uint8_t
>(_buf[3]));
438 if (payloadLen <= 125) {
439 throw std::runtime_error(
"WsFrameReader: non-minimal 16-bit length encoding");
442 }
else if (payloadLen == 127) {
443 if (_buf.size() < 10) {
447 for (std::size_t i = 0; i < 8; ++i) {
448 payloadLen = (payloadLen << 8) | static_cast<std::uint8_t>(_buf[2 + i]);
450 if (payloadLen <= 0xFFFFU) {
451 throw std::runtime_error(
"WsFrameReader: non-minimal 64-bit length encoding");
455 return LengthDecode{.payloadLen = payloadLen, .headerLen = headerLen};
459 [[nodiscard]] std::string extractPayload(std::size_t headerLen, std::size_t maskLen, std::size_t payloadLen,
461 std::string payload = _buf.substr(headerLen + maskLen, payloadLen);
463 std::array<std::uint8_t, 4> key{};
464 for (std::size_t i = 0; i < 4; ++i) {
465 key[i] =
static_cast<std::uint8_t
>(_buf[headerLen + i]);
470 std::array<std::uint8_t, 8> doubled{};
471 std::ranges::copy(key, std::ranges::copy(key, doubled.begin()).out);
472 auto const wideKey = std::bit_cast<std::uint64_t>(doubled);
473 std::span<char> rest{payload};
474 std::uint64_t word = 0;
475 while (rest.size() >=
sizeof word) {
476 std::memcpy(&word, rest.data(),
sizeof word);
478 std::memcpy(rest.data(), &word,
sizeof word);
479 rest = rest.subspan(
sizeof word);
481 std::memcpy(&word, rest.data(), rest.size());
483 std::memcpy(rest.data(), &word, rest.size());
488 static void validateClosePayload(
const std::string& payload) {
489 if (payload.empty()) {
492 bool const validClosePayload =
493 payload.size() >= 2 &&
494 ws_frame_impl::isValidCloseCode(
static_cast<std::uint16_t
>(
495 (
static_cast<std::uint8_t
>(payload.at(0)) << 8) |
static_cast<std::uint8_t
>(payload.at(1))));
496 if (!validClosePayload) {
497 throw std::runtime_error(
"WsFrameReader: close frame carries an invalid status code");
503 ws_frame_impl::Utf8Validator reasonUtf8;
504 if (!reasonUtf8.feed(std::string_view{payload}.substr(2)) || !reasonUtf8.complete()) {
505 throw std::runtime_error(
"WsFrameReader: close frame reason is not valid UTF-8");
509 std::optional<RawFrame> tryExtractRawFrame() {
510 if (_buf.size() < 2) {
513 auto const byte0 =
static_cast<std::uint8_t
>(_buf.at(0));
514 auto const byte1 =
static_cast<std::uint8_t
>(_buf.at(1));
515 auto const opcode = validateFirstByte(byte0);
516 bool const fin = (byte0 & 0x80U) != 0;
517 bool const masked = (byte1 & 0x80U) != 0;
518 validateMaskDirection(masked);
519 std::uint8_t
const lenMarker = byte1 & 0x7FU;
520 validateControlFraming(isControlOpcode(opcode), fin, lenMarker);
522 auto const decoded = decodeLength(lenMarker);
526 if (decoded->payloadLen > ::morph::wire::kMaxEnvelopeBytes) {
527 throw std::runtime_error(
"WsFrameReader: frame payload exceeds kMaxEnvelopeBytes");
529 std::size_t
const maskLen = masked ? 4 : 0;
530 std::size_t
const totalLen = decoded->headerLen + maskLen +
static_cast<std::size_t
>(decoded->payloadLen);
531 if (_buf.size() < totalLen) {
534 std::string payload =
535 extractPayload(decoded->headerLen, maskLen,
static_cast<std::size_t
>(decoded->payloadLen), masked);
536 if (opcode == WsOpcode::kClose) {
537 validateClosePayload(payload);
539 _buf.erase(0, totalLen);
540 return RawFrame{.fin = fin, .opcode = opcode, .payload = std::move(payload)};
550 bool _assembling{
false};
551 WsOpcode _assemblyOpcode{WsOpcode::kText};
552 ws_frame_impl::Utf8Validator _textUtf8;
553 std::string _assembly;