Skip to content

Commit c487f91

Browse files
authored
Fix client Websockets (broken) (#2749)
This PR addresses several issues discovered in the client websocket stack. A simple local websocket echo server has been added for testing, which can be run using `make wsserver`. **Memory leaks** Running `HttpServer_Websockets` sample with valgrind shows a memory leak (details below, fig 1). I can see from the logic of `WebsocketConnection::send` that there are multiple reasons the call could fail, but the `source` stream is only destroyed in one of them. **Failed connection** Testing with the local server failed with `websockets.exceptions.InvalidHeaderValue: invalid Sec-WebSocket-Key header`. The key was 17 bytes instead of 16. **utf-8 decoding errors** Turns out message was getting corrupted because mask value passed to XorStream is on stack, which then gets overwritten before message has been sent out. Fixed by taking a copy of the value. **CLOSE message not being sent** Tested with `Websocket_Client` sample (running local echo server) to confirm correct behaviour, noticed a `Streams without known size are not supported` message when closing the connection. This blocked sending 'CLOSE' notifications which have no payload. **Issues during CLOSE** The TCP socket was being closed too soon, causing additional errors. Increasing timeout to 2 seconds fixes this. Also included a status code in the CLOSE message indicating normal closure; this is optional, but seems like a good thing to do. RFC 6455 states: *The application MUST NOT send any more data frames after sending a Close frame. If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.* Therefore, the `close()` logic has been changed so that a CLOSE message is *always* sent, either in response to a previous incoming request (in which case the received status is echoed back) or as notification that we're closing (status 1000 - normal closure). Checked server operation with `HttpServer_Websockets` sample **Simplify packet generation** It's not necessary to pre-calculate the packet length as it's never more than 14 bytes in length. **WebsocketConnection not getting destroyed** HttpConnection objects are not 'auto-released' so leaks memory every time a WebsocketConnection is destroyed (512+ bytes). Simplest fix for this is to add a `setAutoSelfDestruct` method to `TcpConnection` so this can be changed. **Messages not being received** Connection is activated OK, but `HttpClientConnection` then calls `init` in its `onConnected` handler which resets the new `receive` delegate. This causes incoming websocket frames to be passed to the http parser, instead of the WS parser, hence the `HTTP parser error: HPE_INVALID_CONSTANT` message. ==== Fig 1: Initial memory leak reported by valgrind ``` ==1291918== ==1291918== HEAP SUMMARY: ==1291918== in use at exit: 4,133 bytes in 16 blocks ==1291918== total heap usage: 573 allocs, 557 frees, 71,139 bytes allocated ==1291918== ==1291918== 64 bytes in 2 blocks are definitely lost in loss record 10 of 13 ==1291918== at 0x4041D7D: operator new(unsigned int) (vg_replace_malloc.c:476) ==1291918== by 0x8075BC5: WebsocketConnection::send(char const*, unsigned int, ws_frame_type_t) (WebsocketConnection.cpp:180) ==1291918== by 0x804EB65: send (WebsocketConnection.h:107) ==1291918== by 0x804EB65: sendString (WebsocketConnection.h:145) ==1291918== by 0x804EB65: wsCommandReceived(WebsocketConnection&, String const&) (application.cpp:88) ==1291918== by 0x807575D: operator() (std_function.h:591) ==1291918== by 0x807575D: WebsocketConnection::staticOnDataPayload(void*, char const*, unsigned int) (WebsocketConnection.cpp:128) ==1291918== by 0x8081E5C: ws_parser_execute (ws_parser.c:263) ==1291918== by 0x80756C6: WebsocketConnection::processFrame(TcpClient&, char*, int) (WebsocketConnection.cpp:103) ==1291918== by 0x8079305: operator() (std_function.h:591) ==1291918== by 0x8079305: TcpClient::onReceive(pbuf*) (TcpClient.cpp:150) ==1291918== by 0x8078A8E: TcpConnection::internalOnReceive(pbuf*, signed char) (TcpConnection.cpp:484) ==1291918== by 0x8058560: tcp_input (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== by 0x80627F3: ip4_input (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== by 0x8063C89: ethernet_input (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== by 0x8064245: tapif_select (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== ==1291918== LEAK SUMMARY: ==1291918== definitely lost: 64 bytes in 2 blocks ==1291918== indirectly lost: 0 bytes in 0 blocks ==1291918== possibly lost: 0 bytes in 0 blocks ==1291918== still reachable: 4,069 bytes in 14 blocks ==1291918== suppressed: 0 bytes in 0 blocks ==1291918== Reachable blocks (those to which a pointer was found) are not shown. ==1291918== To see them, rerun with: --leak-check=full --show-leak-kinds=all ==1291918== ==1291918== For lists of detected and suppressed errors, rerun with: -s ==1291918== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) ```
1 parent 7f3eba5 commit c487f91

File tree

12 files changed

+253
-163
lines changed

12 files changed

+253
-163
lines changed

Sming/Components/Network/component.mk

+11
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,14 @@ COMPONENT_INCDIRS += \
9393
endif
9494

9595
endif
96+
97+
##@Testing
98+
99+
# Websocket Server
100+
CACHE_VARS += WSSERVER_PORT
101+
WSSERVER_PORT ?= 9999
102+
.PHONY: wsserver
103+
wsserver: ##Launch a simple python Websocket echo server for testing client applications
104+
$(info Starting Websocket server for TESTING)
105+
$(Q) $(PYTHON) $(CMP_Network_PATH)/tools/wsserver.py $(WSSERVER_PORT)
106+

Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp

+103-86
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@
99
****/
1010

1111
#include "WebsocketConnection.h"
12-
#include <BitManipulations.h>
1312
#include <Crypto/Sha1.h>
1413
#include <Data/WebHelpers/base64.h>
1514
#include <Data/Stream/MemoryDataStream.h>
1615
#include <Data/Stream/XorOutputStream.h>
1716
#include <Data/Stream/SharedMemoryStream.h>
18-
#include <memory>
1917

2018
DEFINE_FSTR(WSSTR_CONNECTION, "connection")
2119
DEFINE_FSTR(WSSTR_UPGRADE, "upgrade")
@@ -32,12 +30,14 @@ WebsocketList WebsocketConnection::websocketList;
3230
/** @brief ws_parser function table
3331
* @note stored in flash memory; as it is word-aligned it can be accessed directly
3432
*/
35-
const ws_parser_callbacks_t WebsocketConnection::parserSettings PROGMEM = {.on_data_begin = staticOnDataBegin,
36-
.on_data_payload = staticOnDataPayload,
37-
.on_data_end = staticOnDataEnd,
38-
.on_control_begin = staticOnControlBegin,
39-
.on_control_payload = staticOnControlPayload,
40-
.on_control_end = staticOnControlEnd};
33+
const ws_parser_callbacks_t WebsocketConnection::parserSettings PROGMEM{
34+
.on_data_begin = staticOnDataBegin,
35+
.on_data_payload = staticOnDataPayload,
36+
.on_data_end = staticOnDataEnd,
37+
.on_control_begin = staticOnControlBegin,
38+
.on_control_payload = staticOnControlPayload,
39+
.on_control_end = staticOnControlEnd,
40+
};
4141

4242
/** @brief Boilerplate code for ws_parser callbacks
4343
* @note Obtain connection object and check it
@@ -55,6 +55,14 @@ WebsocketConnection::WebsocketConnection(HttpConnection* connection, bool isClie
5555
ws_parser_init(&parser);
5656
}
5757

58+
void WebsocketConnection::setConnection(HttpConnection* connection, bool isClientConnection)
59+
{
60+
assert(this->connection == nullptr);
61+
this->connection = connection;
62+
this->isClientConnection = isClientConnection;
63+
this->state = connection ? eWSCS_Ready : eWSCS_Closed;
64+
}
65+
5866
bool WebsocketConnection::bind(HttpRequest& request, HttpResponse& response)
5967
{
6068
String version = request.headers[HTTP_HEADER_SEC_WEBSOCKET_VERSION];
@@ -124,10 +132,21 @@ int WebsocketConnection::staticOnDataPayload(void* userData, const char* at, siz
124132
{
125133
GET_CONNECTION();
126134

127-
if(connection->frameType == WS_FRAME_TEXT && connection->wsMessage) {
128-
connection->wsMessage(*connection, String(at, length));
129-
} else if(connection->frameType == WS_FRAME_BINARY && connection->wsBinary) {
130-
connection->wsBinary(*connection, reinterpret_cast<uint8_t*>(const_cast<char*>(at)), length);
135+
switch(connection->frameType) {
136+
case WS_FRAME_TEXT:
137+
if(connection->wsMessage) {
138+
connection->wsMessage(*connection, String(at, length));
139+
}
140+
break;
141+
case WS_FRAME_BINARY:
142+
if(connection->wsBinary) {
143+
connection->wsBinary(*connection, reinterpret_cast<uint8_t*>(const_cast<char*>(at)), length);
144+
}
145+
break;
146+
case WS_FRAME_CLOSE:
147+
case WS_FRAME_PING:
148+
case WS_FRAME_PONG:
149+
break;
131150
}
132151

133152
return WS_OK;
@@ -142,11 +161,7 @@ int WebsocketConnection::staticOnControlBegin(void* userData, ws_frame_type_t ty
142161
{
143162
GET_CONNECTION();
144163

145-
connection->controlFrame = WsFrameInfo(type, nullptr, 0);
146-
147-
if(type == WS_FRAME_CLOSE) {
148-
connection->close();
149-
}
164+
connection->controlFrame = WsFrameInfo{type};
150165

151166
return WS_OK;
152167
}
@@ -165,13 +180,26 @@ int WebsocketConnection::staticOnControlEnd(void* userData)
165180
{
166181
GET_CONNECTION();
167182

168-
if(connection->controlFrame.type == WS_FRAME_PING) {
183+
switch(connection->controlFrame.type) {
184+
case WS_FRAME_PING:
169185
connection->send(connection->controlFrame.payload, connection->controlFrame.payloadLength, WS_FRAME_PONG);
170-
}
186+
break;
187+
case WS_FRAME_PONG:
188+
if(connection->wsPong) {
189+
connection->wsPong(*connection);
190+
}
191+
break;
192+
193+
case WS_FRAME_CLOSE:
194+
debug_hex(DBG, "WS: CLOSE", connection->controlFrame.payload, connection->controlFrame.payloadLength);
195+
connection->close();
196+
break;
171197

172-
if(connection->controlFrame.type == WS_FRAME_PONG && connection->wsPong) {
173-
connection->wsPong(*connection);
198+
case WS_FRAME_TEXT:
199+
case WS_FRAME_BINARY:
200+
break;
174201
}
202+
175203
return WS_OK;
176204
}
177205

@@ -186,6 +214,7 @@ bool WebsocketConnection::send(const char* message, size_t length, ws_frame_type
186214
size_t written = stream->write(message, length);
187215
if(written != length) {
188216
debug_e("Unable to store data in memory buffer");
217+
delete stream;
189218
return false;
190219
}
191220

@@ -194,96 +223,78 @@ bool WebsocketConnection::send(const char* message, size_t length, ws_frame_type
194223

195224
bool WebsocketConnection::send(IDataSourceStream* source, ws_frame_type_t type, bool useMask, bool isFin)
196225
{
226+
// Ensure source gets destroyed if we return prematurely
227+
std::unique_ptr<IDataSourceStream> sourceRef(source);
228+
197229
if(source == nullptr) {
230+
debug_w("WS: No source");
198231
return false;
199232
}
200233

201234
if(connection == nullptr) {
235+
debug_w("WS: No connection");
202236
return false;
203237
}
204238

205239
if(!activated) {
206-
debug_e("WS Connection is not activated yet!");
240+
debug_e("WS: Not activated");
207241
return false;
208242
}
209243

210244
int available = source->available();
211-
if(available < 1) {
212-
debug_e("Streams without known size are not supported");
245+
if(available < 0) {
246+
debug_e("WS: Unknown stream size");
213247
return false;
214248
}
215249

216-
debug_d("Sending: %d bytes, Type: %d\n", available, type);
217-
218-
size_t packetLength = 2;
219-
uint16_t lengthValue = available;
250+
debug_d("WS: Sending %d bytes, type %d", available, type);
220251

221-
// calculate message length ....
222-
if(available <= 125) {
223-
lengthValue = available;
224-
} else if(available < 65536) {
225-
lengthValue = 126;
226-
packetLength += 2;
227-
} else {
228-
lengthValue = 127;
229-
packetLength += 8;
230-
}
231-
232-
if(useMask) {
233-
packetLength += 4; // we use mask with size 4 bytes
234-
}
235-
236-
uint8_t packet[packetLength];
237-
memset(packet, 0, packetLength);
238-
239-
int i = 0;
240-
// byte 0
252+
// Construct packet
253+
uint8_t packet[16]{};
254+
unsigned len = 0;
241255
if(isFin) {
242-
packet[i] |= bit(7); // set Fin
256+
packet[len] |= _BV(7); // set Fin
243257
}
244-
packet[i++] |= (uint8_t)type; // set opcode
245-
// byte 1
258+
packet[len++] |= type; // set opcode
246259
if(useMask) {
247-
packet[i] |= bit(7); // set mask
260+
packet[len] |= _BV(7); // set mask
248261
}
249-
250262
// length
251-
if(lengthValue < 126) {
252-
packet[i++] |= lengthValue;
253-
} else if(lengthValue == 126) {
254-
packet[i++] |= 126;
255-
packet[i++] = (available >> 8) & 0xFF;
256-
packet[i++] = available & 0xFF;
257-
} else if(lengthValue == 127) {
258-
packet[i++] |= 127;
259-
packet[i++] = 0;
260-
packet[i++] = 0;
261-
packet[i++] = 0;
262-
packet[i++] = 0;
263-
packet[i++] = (available >> 24) & 0xFF;
264-
packet[i++] = (available >> 16) & 0xFF;
265-
packet[i++] = (available >> 8) & 0xFF;
266-
packet[i++] = (available)&0xFF;
263+
if(available <= 125) {
264+
packet[len++] |= available;
265+
} else if(available <= 0xffff) {
266+
packet[len++] |= 126;
267+
packet[len++] = available >> 8;
268+
packet[len++] = available;
269+
} else {
270+
packet[len++] |= 127;
271+
len += 4; // All 0
272+
packet[len++] = available >> 24;
273+
packet[len++] = available >> 16;
274+
packet[len++] = available >> 8;
275+
packet[len++] = available;
267276
}
268-
269277
if(useMask) {
270-
uint8_t maskKey[4] = {0x00, 0x00, 0x00, 0x00};
271-
for(uint8_t x = 0; x < sizeof(maskKey); x++) {
272-
maskKey[x] = (char)os_random();
273-
packet[i++] = maskKey[x];
274-
}
278+
uint8_t maskKey[4];
279+
os_get_random(maskKey, sizeof(maskKey));
280+
memcpy(&packet[len], maskKey, sizeof(maskKey));
281+
len += sizeof(maskKey);
275282

276283
auto xorStream = new XorOutputStream(source, maskKey, sizeof(maskKey));
277-
source = xorStream;
284+
if(xorStream == nullptr) {
285+
return false;
286+
}
287+
sourceRef.release();
288+
sourceRef.reset(xorStream);
278289
}
279290

280291
// send the header
281-
if(!connection->send(reinterpret_cast<const char*>(packet), packetLength)) {
282-
delete source;
292+
if(!connection->send(reinterpret_cast<const char*>(packet), len)) {
283293
return false;
284294
}
285295

286-
return connection->send(source);
296+
// Pass stream to connection
297+
return connection->send(sourceRef.release());
287298
}
288299

289300
void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame_type_t type)
@@ -300,23 +311,29 @@ void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame
300311

301312
void WebsocketConnection::close()
302313
{
303-
debug_d("Terminating Websocket connection.");
314+
if(connection == nullptr) {
315+
return;
316+
}
317+
318+
debug_d("WS: Terminating connection %p, state %u", connection, state);
304319
websocketList.removeElement(this);
305320
if(state != eWSCS_Closed) {
306321
state = eWSCS_Closed;
307-
if(isClientConnection) {
308-
send(nullptr, 0, WS_FRAME_CLOSE);
322+
if(controlFrame.type == WS_FRAME_CLOSE) {
323+
send(controlFrame.payload, controlFrame.payloadLength, WS_FRAME_CLOSE);
324+
} else {
325+
uint16_t status = htons(1000);
326+
send(reinterpret_cast<char*>(&status), sizeof(status), WS_FRAME_CLOSE);
309327
}
310328
activated = false;
311329
if(wsDisconnect) {
312330
wsDisconnect(*this);
313331
}
314332
}
315333

316-
if(connection) {
317-
connection->setTimeOut(1);
318-
connection = nullptr;
319-
}
334+
connection->setTimeOut(2);
335+
connection->setAutoSelfDestruct(true);
336+
connection = nullptr;
320337
}
321338

322339
void WebsocketConnection::reset()

Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.h

+6-18
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,6 @@ struct WsFrameInfo {
5656
ws_frame_type_t type = WS_FRAME_TEXT;
5757
char* payload = nullptr;
5858
size_t payloadLength = 0;
59-
60-
WsFrameInfo() = default;
61-
62-
WsFrameInfo(ws_frame_type_t type, char* payload, size_t payloadLength)
63-
: type(type), payload(payload), payloadLength(payloadLength)
64-
{
65-
}
6659
};
6760

6861
class WebsocketConnection
@@ -73,7 +66,7 @@ class WebsocketConnection
7366
* @param connection the transport connection
7467
* @param isClientConnection true when the passed connection is an http client connection
7568
*/
76-
WebsocketConnection(HttpConnection* connection, bool isClientConnection = true);
69+
WebsocketConnection(HttpConnection* connection = nullptr, bool isClientConnection = true);
7770

7871
virtual ~WebsocketConnection()
7972
{
@@ -109,14 +102,14 @@ class WebsocketConnection
109102

110103
/**
111104
* @brief Sends websocket message from a stream
112-
* @param stream
105+
* @param source The stream to send - we get ownership of the stream
113106
* @param type
114107
* @param useMask MUST be true for client connections
115108
* @param isFin true if this is the final frame
116109
*
117110
* @retval bool true on success
118111
*/
119-
bool send(IDataSourceStream* stream, ws_frame_type_t type = WS_FRAME_TEXT, bool useMask = false, bool isFin = true);
112+
bool send(IDataSourceStream* source, ws_frame_type_t type = WS_FRAME_TEXT, bool useMask = false, bool isFin = true);
120113

121114
/**
122115
* @brief Broadcasts a message to all active websocket connections
@@ -271,11 +264,7 @@ class WebsocketConnection
271264
* @param connection the transport connection
272265
* @param isClientConnection true when the passed connection is an http client connection
273266
*/
274-
void setConnection(HttpConnection* connection, bool isClientConnection = true)
275-
{
276-
this->connection = connection;
277-
this->isClientConnection = isClientConnection;
278-
}
267+
void setConnection(HttpConnection* connection, bool isClientConnection = true);
279268

280269
/** @brief Gets the state of the websocket connection
281270
* @retval WsConnectionState
@@ -311,7 +300,7 @@ class WebsocketConnection
311300

312301
void* userData = nullptr;
313302

314-
WsConnectionState state = eWSCS_Ready;
303+
WsConnectionState state;
315304

316305
private:
317306
ws_frame_type_t frameType = WS_FRAME_TEXT;
@@ -322,9 +311,8 @@ class WebsocketConnection
322311

323312
static WebsocketList websocketList;
324313

325-
bool isClientConnection = true;
326-
327314
HttpConnection* connection = nullptr;
315+
bool isClientConnection;
328316
bool activated = false;
329317
};
330318

0 commit comments

Comments
 (0)