-
Notifications
You must be signed in to change notification settings - Fork 89
Fix broken WebSocket defragmentation #383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,10 @@ | |
| #include <memory> | ||
| #include <utility> | ||
|
|
||
| #define STATE_FRAME_START 0 | ||
| #define STATE_FRAME_MASK 1 | ||
| #define STATE_FRAME_DATA 2 | ||
|
|
||
| using namespace asyncsrv; | ||
|
|
||
| size_t webSocketSendFrameWindow(AsyncClient *client) { | ||
|
|
@@ -226,8 +230,8 @@ const char *AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING"; | |
| const size_t AWSC_PING_PAYLOAD_LEN = 22; | ||
|
|
||
| AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server) | ||
| : _client(client), _server(server), _clientId(_server->_getNextId()), _status(WS_CONNECTED), _pstate(0), _lastMessageTime(millis()), _keepAlivePeriod(0), | ||
| _tempObject(NULL) { | ||
| : _client(client), _server(server), _clientId(_server->_getNextId()), _status(WS_CONNECTED), _pstate(STATE_FRAME_START), _lastMessageTime(millis()), | ||
| _keepAlivePeriod(0), _tempObject(NULL) { | ||
|
|
||
| _client->setRxTimeout(0); | ||
| _client->onError( | ||
|
|
@@ -508,8 +512,11 @@ void AsyncWebSocketClient::_onDisconnect() { | |
| void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | ||
| _lastMessageTime = millis(); | ||
| uint8_t *data = (uint8_t *)pbuf; | ||
|
|
||
| while (plen > 0) { | ||
| if (!_pstate) { | ||
| async_ws_log_v("WS[%" PRIu32 "]: _onData: plen=%" PRIu32 ", _pstate=%" PRIu8 ", _status=%" PRIu8, _clientId, plen, _pstate, static_cast<uint8_t>(_status)); | ||
|
|
||
| if (_pstate == STATE_FRAME_START) { | ||
| const uint8_t *fdata = data; | ||
|
|
||
| _pinfo.index = 0; | ||
|
|
@@ -518,13 +525,6 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | |
| _pinfo.masked = ((fdata[1] & 0x80) != 0) ? 1 : 0; | ||
| _pinfo.len = fdata[1] & 0x7F; | ||
|
|
||
| // async_ws_log_w("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen); | ||
| // async_ws_log_w("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status); | ||
| // async_ws_log_w( | ||
| // "WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, | ||
| // _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len | ||
| // ); | ||
|
|
||
| data += 2; | ||
| plen -= 2; | ||
|
|
||
|
|
@@ -541,47 +541,52 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | |
| } | ||
| } | ||
|
|
||
| if (_pinfo.masked) { | ||
| // Read mask bytes (may be fragmented across packets in Safari) | ||
| size_t mask_offset = 0; | ||
|
|
||
| // If we're resuming from a previous fragmented read, check _pinfo.index | ||
| if (_pstate == 1 && _pinfo.index < 4) { | ||
| mask_offset = _pinfo.index; | ||
| } | ||
|
|
||
| // Read as many mask bytes as available | ||
| while (mask_offset < 4 && plen > 0) { | ||
| _pinfo.mask[mask_offset++] = *data++; | ||
| plen--; | ||
| } | ||
|
|
||
| // Check if we have all 4 mask bytes | ||
| if (mask_offset < 4) { | ||
| // Incomplete mask | ||
| if (_pinfo.opcode == WS_DISCONNECT && plen == 0) { | ||
| // Safari close frame edge case: masked bit set but no mask data | ||
| // async_ws_log_w("WS[%" PRIu32 "]: close frame with incomplete mask, treating as unmasked", _clientId); | ||
| async_ws_log_v( | ||
| "WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, | ||
| _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len | ||
| ); | ||
|
|
||
| // Handle fragmented mask data - Safari may split the 4-byte mask across multiple packets | ||
| // _pinfo.masked is 1 if we need to start reading mask bytes | ||
| // _pinfo.masked is 2, 3, or 4 if we have partially read the mask | ||
| // _pinfo.masked is 5 if the mask is complete | ||
| while (_pinfo.masked && _pstate <= STATE_FRAME_MASK && _pinfo.masked < 5) { | ||
| // check if we have some data | ||
| if (plen == 0) { | ||
| // Safari close frame edge case: masked bit set but no mask data | ||
| if (_pinfo.opcode == WS_DISCONNECT) { | ||
| async_ws_log_v("WS[%" PRIu32 "]: close frame with incomplete mask, treating as unmasked", _clientId); | ||
| _pinfo.masked = 0; | ||
| _pinfo.index = 0; | ||
| } else { | ||
| // Wait for more data | ||
| // async_ws_log_w("WS[%" PRIu32 "]: waiting for more mask data: read=%zu/4", _clientId, mask_offset); | ||
| _pinfo.index = mask_offset; // Save progress | ||
| _pstate = 1; | ||
| return; | ||
| _pinfo.len = 0; | ||
|
Comment on lines
560
to
+561
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To go into the disconnect use case below |
||
| _pstate = STATE_FRAME_START; | ||
| break; | ||
| } | ||
| } else { | ||
| // All mask bytes received | ||
| // async_ws_log_w("WS[%" PRIu32 "]: mask complete", _clientId); | ||
| _pinfo.index = 0; // Reset index for payload processing | ||
|
|
||
| //wait for more data | ||
| _pstate = STATE_FRAME_MASK; | ||
| async_ws_log_v("WS[%" PRIu32 "]: waiting for more mask data: read=%" PRIu8 "/4", _clientId, _pinfo.masked - 1); | ||
| return; | ||
| } | ||
|
|
||
| // accumulate mask bytes | ||
| _pinfo.mask[_pinfo.masked - 1] = data[0]; | ||
| data += 1; | ||
| plen -= 1; | ||
| _pinfo.masked++; | ||
| } | ||
mathieucarbou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); | ||
| const auto datalast = data[datalen]; | ||
| // all mask bytes read if we were reading them | ||
| _pstate = STATE_FRAME_DATA; | ||
|
|
||
| // async_ws_log_w("WS[%" PRIu32 "]: _processing data: datalen=%" PRIu32 ", plen=%" PRIu32, _clientId, datalen, plen); | ||
| // restore masked to 1 for backward compatibility | ||
| if (_pinfo.masked >= 5) { | ||
| async_ws_log_v("WS[%" PRIu32 "]: mask read complete", _clientId); | ||
| _pinfo.masked = 1; | ||
| } | ||
|
|
||
| const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen); | ||
| const auto datalast = datalen ? data[datalen] : 0; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really hate this thing... users have to add |
||
|
|
||
| if (_pinfo.masked) { | ||
| for (size_t i = 0; i < datalen; i++) { | ||
|
|
@@ -590,22 +595,30 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | |
| } | ||
|
|
||
| if ((datalen + _pinfo.index) < _pinfo.len) { | ||
| _pstate = 1; | ||
| _pstate = STATE_FRAME_DATA; | ||
| async_ws_log_v("WS[%" PRIu32 "]: processing next fragment index=%" PRIu64 ", len=%" PRIu32 "", _clientId, _pinfo.index, (uint32_t)datalen); | ||
|
|
||
| if (_pinfo.index == 0) { | ||
| if (_pinfo.opcode) { | ||
| _pinfo.message_opcode = _pinfo.opcode; | ||
| _pinfo.num = 0; | ||
| } | ||
| } | ||
|
|
||
| if (datalen > 0) { | ||
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen); | ||
| } | ||
|
|
||
| // track index for next fragment | ||
| _pinfo.index += datalen; | ||
|
|
||
| } else if ((datalen + _pinfo.index) == _pinfo.len) { | ||
| _pstate = 0; | ||
| _pstate = STATE_FRAME_START; | ||
| async_ws_log_v("WS[%" PRIu32 "]: processing final fragment index=%" PRIu64 ", len=%" PRIu32 "", _clientId, _pinfo.index, (uint32_t)datalen); | ||
|
|
||
| if (_pinfo.opcode == WS_DISCONNECT) { | ||
| async_ws_log_v("WS[%" PRIu32 "]: processing disconnect", _clientId); | ||
|
|
||
| if (datalen) { | ||
| uint16_t reasonCode = (uint16_t)(data[0] << 8) + data[1]; | ||
| char *reasonString = (char *)(data + 2); | ||
|
|
@@ -625,24 +638,39 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) { | |
| } | ||
| _queueControl(WS_DISCONNECT, data, datalen); | ||
| } | ||
|
|
||
| } else if (_pinfo.opcode == WS_PING) { | ||
| async_ws_log_v("WS[%" PRIu32 "]: processing ping", _clientId); | ||
| _server->_handleEvent(this, WS_EVT_PING, NULL, NULL, 0); | ||
| _queueControl(WS_PONG, data, datalen); | ||
|
|
||
| } else if (_pinfo.opcode == WS_PONG) { | ||
| async_ws_log_v("WS[%" PRIu32 "]: processing pong", _clientId); | ||
| if (datalen != AWSC_PING_PAYLOAD_LEN || memcmp(AWSC_PING_PAYLOAD, data, AWSC_PING_PAYLOAD_LEN) != 0) { | ||
| _server->_handleEvent(this, WS_EVT_PONG, NULL, NULL, 0); | ||
| } | ||
|
|
||
| } else if (_pinfo.opcode < WS_DISCONNECT) { // continuation or text/binary frame | ||
| async_ws_log_v("WS[%" PRIu32 "]: processing data frame num=%" PRIu32 "", _clientId, _pinfo.num); | ||
| _server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen); | ||
| if (_pinfo.final) { | ||
| _pinfo.num = 0; | ||
| } else { | ||
| _pinfo.num += 1; | ||
| } | ||
| } | ||
|
|
||
| } else { | ||
| // async_ws_log_w("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); | ||
| // what should we do? | ||
| // unexpected frame error, close connection | ||
| _pstate = STATE_FRAME_START; | ||
|
|
||
| async_ws_log_v("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len); | ||
|
|
||
| _status = WS_DISCONNECTING; | ||
| if (_client) { | ||
| _client->ackLater(); | ||
| } | ||
| _queueControl(WS_DISCONNECT, data, datalen); | ||
|
Comment on lines
+669
to
+673
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we receive an invalid frame, nothing were done so the connection would be left in an unknown state, including frame defragmentation... So let's force a disconnect to restore the connection. it could also be a rogue client... |
||
| break; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kept all logs
async_ws_log_vto still be able to easily debug