Skip to content

Commit ee4495a

Browse files
committed
Handle edge-case in which we receive a partial WS header
1 parent 1d46269 commit ee4495a

File tree

2 files changed

+99
-16
lines changed

2 files changed

+99
-16
lines changed

src/AsyncWebSocket.cpp

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,8 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
473473
_clientId = _server->_getNextId();
474474
_status = WS_CONNECTED;
475475
_pstate = 0;
476+
_partialHeader = nullptr;
477+
_partialHeaderLen = 0;
476478
_lastMessageTime = millis();
477479
_keepAlivePeriod = 0;
478480
_client->setRxTimeout(0);
@@ -611,30 +613,107 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen){
611613
_lastMessageTime = millis();
612614
uint8_t *data = (uint8_t*)pbuf;
613615
while(plen > 0){
614-
if(!_pstate){
615-
const uint8_t *fdata = data;
616+
if(!_pstate) {
617+
ssize_t dataPayloadOffset = 0;
618+
const uint8_t *headerBuf = data;
619+
620+
// plen is backed up to initialPlen because, in case we receive a partial header, we would like to undo all of our
621+
// parsing and copy all of what we have of the header into a buffer for later use.
622+
// plen is modified during the parsing attempt, so if we don't back it up we won't know how much we need to copy.
623+
// partialHeaderLen is also backed up for the same reason.
624+
size_t initialPlen = plen;
625+
size_t partialHeaderLen = 0;
626+
627+
if (_partialHeaderLen > 0) {
628+
// We previously received a truncated header. Recover it by doing the following:
629+
// - Copy the new header chunk into the previous partial header, filling the buffer. It is allocated as a
630+
// buffer in a class field.
631+
// - Change *headerBuf to point to said buffer
632+
// - Update the length counters so that:
633+
// - The initialPlen and plen, which refer to the length of the remaining packet data, also accounts for the
634+
// previously received truncated header
635+
// - The dataPayloadOffset, which is the offset after the header at which the payload begins, so that it
636+
// refers to a point potentially before the beginning of the buffer. As we parse the header we increment it,
637+
// and we can pretty much guarantee it will go back to being positive unless there is a major bug.
638+
// - The class _partialHeaderLen is back to zero since we took ownership of the contained data.
639+
memcpy(_partialHeader + _partialHeaderLen, data,
640+
std::min(plen, (size_t) WS_MAX_HEADER_LEN - _partialHeaderLen));
641+
headerBuf = _partialHeader;
642+
initialPlen += _partialHeaderLen;
643+
plen += _partialHeaderLen;
644+
dataPayloadOffset -= _partialHeaderLen;
645+
partialHeaderLen = _partialHeaderLen;
646+
647+
_partialHeaderLen = 0;
648+
}
649+
650+
// The following series of gotos could have been a try-catch but we are likely being built with -fno-exceptions
651+
if (plen < 2)
652+
goto _exceptionHandleFailPartialHeader;
653+
616654
_pinfo.index = 0;
617-
_pinfo.final = (fdata[0] & 0x80) != 0;
618-
_pinfo.opcode = fdata[0] & 0x0F;
619-
_pinfo.masked = (fdata[1] & 0x80) != 0;
620-
_pinfo.len = fdata[1] & 0x7F;
621-
data += 2;
655+
_pinfo.final = (headerBuf[0] & 0x80) != 0;
656+
_pinfo.opcode = headerBuf[0] & 0x0F;
657+
_pinfo.masked = (headerBuf[1] & 0x80) != 0;
658+
_pinfo.len = headerBuf[1] & 0x7F;
659+
dataPayloadOffset += 2;
622660
plen -= 2;
623-
if(_pinfo.len == 126){
624-
_pinfo.len = fdata[3] | (uint16_t)(fdata[2]) << 8;
625-
data += 2;
661+
662+
if (_pinfo.len == 126) {
663+
if (plen < 2)
664+
goto _exceptionHandleFailPartialHeader;
665+
666+
_pinfo.len = headerBuf[3] | (uint16_t)(headerBuf[2]) << 8;
667+
dataPayloadOffset += 2;
626668
plen -= 2;
627-
} else if(_pinfo.len == 127){
628-
_pinfo.len = fdata[9] | (uint16_t)(fdata[8]) << 8 | (uint32_t)(fdata[7]) << 16 | (uint32_t)(fdata[6]) << 24 | (uint64_t)(fdata[5]) << 32 | (uint64_t)(fdata[4]) << 40 | (uint64_t)(fdata[3]) << 48 | (uint64_t)(fdata[2]) << 56;
629-
data += 8;
669+
} else if (_pinfo.len == 127) {
670+
if (plen < 8)
671+
goto _exceptionHandleFailPartialHeader;
672+
673+
_pinfo.len = headerBuf[9] | (uint16_t)(headerBuf[8]) << 8 | (uint32_t)(headerBuf[7]) << 16 |
674+
(uint32_t)(headerBuf[6]) << 24 | (uint64_t)(headerBuf[5]) << 32 | (uint64_t)(headerBuf[4]) << 40 |
675+
(uint64_t)(headerBuf[3]) << 48 | (uint64_t)(headerBuf[2]) << 56;
676+
dataPayloadOffset += 8;
630677
plen -= 8;
631678
}
632679

633-
if(_pinfo.masked){
634-
memcpy(_pinfo.mask, data, 4);
635-
data += 4;
680+
if (_pinfo.masked) {
681+
if (plen < 4)
682+
goto _exceptionHandleFailPartialHeader;
683+
684+
memcpy(_pinfo.mask, headerBuf + dataPayloadOffset + partialHeaderLen, 4);
685+
dataPayloadOffset += 4;
636686
plen -= 4;
637687
}
688+
689+
// Yes I know the control flow here isn't 100% legible but we must support -fno-exceptions.
690+
// If we got to this point it means we did NOT receive a truncated header, therefore we can skip the exception
691+
// handling.
692+
// Control flow resumes after the following block.
693+
goto _headerParsingSuccessful;
694+
695+
// We DID receive a truncated header:
696+
// - We copy it to our buffer and set the _partialHeaderLen
697+
// - We return early
698+
// This will trigger the partial recovery at the next call of this method, once more data is received and we have
699+
// a full header.
700+
_exceptionHandleFailPartialHeader:
701+
{
702+
if (initialPlen <= WS_MAX_HEADER_LEN) {
703+
// If initialPlen > WS_MAX_HEADER_LEN there must be something wrong with this code. It should never happen but
704+
// but it's better safe than sorry.
705+
memcpy(_partialHeader, headerBuf, initialPlen * sizeof(uint8_t));
706+
_partialHeaderLen = initialPlen;
707+
} else {
708+
DEBUGF("[AsyncWebSocketClient::_onData] initialPlen (= %d) > WS_MAX_HEADER_LEN (= %d)\n", initialPlen,
709+
WS_MAX_HEADER_LEN);
710+
}
711+
return;
712+
}
713+
714+
_headerParsingSuccessful:
715+
716+
data += dataPayloadOffset;
638717
}
639718

640719
const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen);

src/AsyncWebSocket.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
#define DEFAULT_MAX_WS_CLIENTS 4
4747
#endif
4848

49+
#define WS_MAX_HEADER_LEN 16
50+
4951
class AsyncWebSocket;
5052
class AsyncWebSocketResponse;
5153
class AsyncWebSocketClient;
@@ -166,6 +168,8 @@ class AsyncWebSocketClient {
166168

167169
uint8_t _pstate;
168170
AwsFrameInfo _pinfo;
171+
uint8_t _partialHeader[WS_MAX_HEADER_LEN];
172+
uint8_t _partialHeaderLen;
169173

170174
uint32_t _lastMessageTime;
171175
uint32_t _keepAlivePeriod;

0 commit comments

Comments
 (0)