Skip to content

Commit 9d2eef5

Browse files
committed
feat(transport_ws): add support for per-message compression handshakes
1 parent ab14938 commit 9d2eef5

File tree

2 files changed

+225
-4
lines changed

2 files changed

+225
-4
lines changed

components/tcp_transport/include/esp_transport_ws.h

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ typedef enum ws_transport_opcodes {
2424
WS_TRANSPORT_OPCODES_CLOSE = 0x08,
2525
WS_TRANSPORT_OPCODES_PING = 0x09,
2626
WS_TRANSPORT_OPCODES_PONG = 0x0a,
27+
WS_TRANSPORT_OPCODES_COMPRESSED = 0x40,
2728
WS_TRANSPORT_OPCODES_FIN = 0x80,
2829
WS_TRANSPORT_OPCODES_NONE = 0x100, /*!< not a valid opcode to indicate no message previously received
2930
* from the API esp_transport_ws_get_read_opcode() */
@@ -48,6 +49,13 @@ typedef struct {
4849
* If false, only user frames are propagated, control frames are handled
4950
* automatically during read operations
5051
*/
52+
bool per_msg_compress; /*!< Hint the server to enable per-message compression (RFC7692) */
53+
int per_msg_client_deflate_window_bit; /*!< Hint the server Per-message deflate window bit 8 to 15; or leave 0 to let server decide */
54+
int per_msg_server_deflate_window_bit; /*!< Hint the server Per-message deflate window bit 8 to 15; or leave 0 to let server decide */
55+
bool per_msg_server_no_ctx_takeover; /*!< Hint the server to reset the compression stream on every WS frame on server side
56+
* True for a safer transfer, false for better performance */
57+
bool per_msg_client_no_ctx_takeover; /*!< Hint the server to reset the compression stream on every WS frame on client side
58+
* True for a safer transfer, false for better performance */
5159
} esp_transport_ws_config_t;
5260

5361
/**
@@ -184,6 +192,78 @@ int esp_transport_ws_send_raw(esp_transport_handle_t t, ws_transport_opcodes_t o
184192
*/
185193
bool esp_transport_ws_get_fin_flag(esp_transport_handle_t t);
186194

195+
/**
196+
* @brief Returns the RSV1 flag (permessage-deflate) of the last read frame
197+
*
198+
* @param[in] t The transport handle
199+
*
200+
* @return
201+
* - true if the last read frame was compressed
202+
* - false otherwise
203+
*/
204+
bool esp_transport_ws_get_rsv1_flag(esp_transport_handle_t t);
205+
206+
/**
207+
* @brief Get per-message compression flag
208+
*
209+
* @param[in] t The transport handle
210+
*
211+
* @return
212+
* - true if per-message compression is enabled
213+
* - false if per-message compression is disabled
214+
*/
215+
bool esp_transport_ws_get_per_msg_compress(esp_transport_handle_t t);
216+
217+
/**
218+
* @brief Get client deflate window bit for per-message compression
219+
*
220+
* @param[in] t The transport handle
221+
*
222+
* @return
223+
* - client deflate window bit
224+
*/
225+
int esp_transport_ws_get_per_msg_client_deflate_window_bit(esp_transport_handle_t t);
226+
227+
/**
228+
* @brief Get server deflate window bit for per-message compression
229+
*
230+
* @param[in] t The transport handle
231+
*
232+
* @return
233+
* - server deflate window bit
234+
*/
235+
int esp_transport_ws_get_per_msg_server_deflate_window_bit(esp_transport_handle_t t);
236+
237+
/**
238+
* @brief Get server no context takeover flag for per-message compression
239+
*
240+
* If this is returned to be true, then the server-to-client's compression handle should be reset
241+
* on every frame transfer. If this is false, then the server-to-client's compression handle
242+
* should not be reset over the lifespan of this esp_transport_handle_t.
243+
*
244+
* @param[in] t The transport handle
245+
*
246+
* @return
247+
* - true if server no context takeover is enabled
248+
* - false if server no context takeover is disabled
249+
*/
250+
bool esp_transport_ws_get_per_msg_server_no_ctx_takeover(esp_transport_handle_t t);
251+
252+
/**
253+
* @brief Get client no context takeover flag for per-message compression
254+
*
255+
* If this is returned to be true, then the client-to-server's compression handle should be reset
256+
* on every frame transfer. If this is false, then the client-to-server's compression handle
257+
* should not be reset over the lifespan of this esp_transport_handle_t.
258+
*
259+
* @param[in] t The transport handle
260+
*
261+
* @return
262+
* - true if client no context takeover is enabled
263+
* - false if client no context takeover is disabled
264+
*/
265+
bool esp_transport_ws_get_per_msg_client_no_ctx_takeover(esp_transport_handle_t t);
266+
187267
/**
188268
* @brief Returns the HTTP status code of the websocket handshake
189269
*

components/tcp_transport/transport_ws.c

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ static const char *TAG = "transport_ws";
2424

2525
#define WS_BUFFER_SIZE CONFIG_WS_BUFFER_SIZE
2626
#define WS_FIN 0x80
27+
#define WS_COMPRESSED 0x40
2728
#define WS_OPCODE_CONT 0x00
2829
#define WS_OPCODE_TEXT 0x01
2930
#define WS_OPCODE_BINARY 0x02
@@ -56,6 +57,7 @@ typedef struct {
5657
int payload_len; /*!< Total length of the payload */
5758
int bytes_remaining; /*!< Bytes left to read of the payload */
5859
bool header_received; /*!< Flag to indicate that a new message header was received */
60+
bool compressed; /*!< Per-message deflate compress flag (RSV1) */
5961
} ws_transport_frame_state_t;
6062

6163
typedef struct {
@@ -75,6 +77,11 @@ typedef struct {
7577
char *redir_host;
7678
char *response_header;
7779
size_t response_header_len;
80+
bool per_msg_compress;
81+
int per_msg_client_deflate_window_bit;
82+
int per_msg_server_deflate_window_bit;
83+
bool per_msg_server_no_ctx_takeover;
84+
bool per_msg_client_no_ctx_takeover;
7885
} transport_ws_t;
7986

8087
/**
@@ -201,6 +208,32 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
201208
#endif
202209

203210
size_t outlen = 0;
211+
char extension_header[160] = { 0 };
212+
if (ws->per_msg_compress) {
213+
int offset = snprintf(extension_header, sizeof(extension_header), "Sec-WebSocket-Extensions: permessage-deflate");
214+
if (ws->per_msg_client_no_ctx_takeover) {
215+
offset += snprintf(extension_header + offset, sizeof(extension_header) - offset, "; client_no_context_takeover");
216+
}
217+
218+
if (ws->per_msg_server_no_ctx_takeover) {
219+
offset += snprintf(extension_header + offset, sizeof(extension_header) - offset, "; server_no_context_takeover");
220+
}
221+
222+
// If this is 0 then it means to let server decide the client window bit
223+
if (ws->per_msg_client_deflate_window_bit != 0) {
224+
offset += snprintf(extension_header + offset, sizeof(extension_header) - offset, "; client_max_window_bits=%d", ws->per_msg_client_deflate_window_bit);
225+
} else {
226+
offset += snprintf(extension_header + offset, sizeof(extension_header) - offset, "; client_max_window_bits");
227+
}
228+
229+
// If this is 0 then it means to let server decide the server window bit
230+
if (ws->per_msg_server_deflate_window_bit != 0) {
231+
offset += snprintf(extension_header + offset, sizeof(extension_header) - offset, "; server_max_window_bits=%d", ws->per_msg_server_deflate_window_bit);
232+
}
233+
234+
snprintf(extension_header + offset, sizeof(extension_header) - offset, "\r\n");
235+
}
236+
204237
esp_crypto_base64_encode(client_key, sizeof(client_key), &outlen, random_key, sizeof(random_key));
205238
int len = snprintf(ws->buffer, WS_BUFFER_SIZE,
206239
"GET %s HTTP/1.1\r\n"
@@ -209,10 +242,12 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
209242
"User-Agent: %s\r\n"
210243
"Upgrade: websocket\r\n"
211244
"Sec-WebSocket-Version: 13\r\n"
212-
"Sec-WebSocket-Key: %s\r\n",
245+
"Sec-WebSocket-Key: %s\r\n"
246+
"%s", // For "Sec-WebSocket-Extensions"
213247
ws->path,
214248
host, port, user_agent_ptr,
215-
client_key);
249+
client_key,
250+
extension_header);
216251
if (len <= 0 || len >= WS_BUFFER_SIZE) {
217252
ESP_LOGE(TAG, "Error in request generation, desired request len: %d, buffer size: %d", len, WS_BUFFER_SIZE);
218253
return -1;
@@ -306,6 +341,9 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
306341
}
307342
header_cursor += strlen("\r\n");
308343

344+
// If compression was requested, we need to check server response
345+
bool pmd_negotiated = false;
346+
309347
while(header_cursor < delim_ptr){
310348
const char * end_of_line = strnstr(header_cursor, "\r\n", header_len - (header_cursor - ws->buffer));
311349
if(!end_of_line){
@@ -332,6 +370,53 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
332370
server_key = header_cursor + header_sec_websocket_accept_len;
333371
server_key_len = line_len - header_sec_websocket_accept_len;
334372
}
373+
// Check for Sec-WebSocket-Extensions header
374+
else if (ws->per_msg_compress && line_len >= strlen("Sec-WebSocket-Extensions: ") && !strncasecmp(header_cursor, "Sec-WebSocket-Extensions: ", strlen("Sec-WebSocket-Extensions: "))) {
375+
const char* ext_params = header_cursor + strlen("Sec-WebSocket-Extensions: ");
376+
int ext_params_len = line_len - strlen("Sec-WebSocket-Extensions: ");
377+
ESP_LOGD(TAG, "Found Sec-WebSocket-Extensions: %.*s", ext_params_len, ext_params);
378+
379+
if (strcasestr(ext_params, "permessage-deflate")) {
380+
pmd_negotiated = true;
381+
382+
// Server must agree to context takeover settings
383+
if (!strcasestr(ext_params, "server_no_context_takeover")) {
384+
ws->per_msg_server_no_ctx_takeover = false;
385+
}
386+
if (!strcasestr(ext_params, "client_no_context_takeover")) {
387+
ws->per_msg_client_no_ctx_takeover = false;
388+
}
389+
390+
const char *smwb_str = "server_max_window_bits=";
391+
const char *found = strcasestr(ext_params, smwb_str);
392+
if (found) {
393+
char *endptr;
394+
long smwb = strtol(found + strlen(smwb_str), &endptr, 10);
395+
if (smwb < 8 || smwb > 15) {
396+
ESP_LOGE(TAG, "compression: Server Max Window Bits is invalid: %ld", smwb);
397+
return -1;
398+
}
399+
400+
ws->per_msg_server_deflate_window_bit = (int)smwb;
401+
} else {
402+
ws->per_msg_server_deflate_window_bit = 15;
403+
}
404+
405+
const char *cmwb_str = "client_max_window_bits=";
406+
found = strcasestr(ext_params, cmwb_str);
407+
if (found) {
408+
char *endptr;
409+
long cmwb = strtol(found + strlen(cmwb_str), &endptr, 10);
410+
411+
if (cmwb < 8 || cmwb > 15) {
412+
ESP_LOGE(TAG, "compression: Client Max Window Bits is invalid: %ld", cmwb);
413+
return -1;
414+
}
415+
416+
ws->per_msg_client_deflate_window_bit = (int)cmwb;
417+
}
418+
}
419+
}
335420
else if (ws->header_hook) {
336421
ws->header_hook(ws->header_user_context, header_cursor, line_len);
337422
}
@@ -349,6 +434,10 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
349434
header_cursor += strlen("\r\n");
350435
}
351436

437+
if (ws->per_msg_compress && !pmd_negotiated) {
438+
ws->per_msg_compress = false;
439+
}
440+
352441
if (WS_HTTP_TEMPORARY_REDIRECT(ws->http_status_code) || WS_HTTP_PERMANENT_REDIRECT(ws->http_status_code)) {
353442
if (location == NULL || location_len <= 0) {
354443
ESP_LOGE(TAG, "Location header not found");
@@ -575,6 +664,7 @@ static int ws_read_header(esp_transport_handle_t t, char *buffer, int len, int t
575664
ws->frame_state.header_received = true;
576665
ws->frame_state.fin = (*data_ptr & 0x80) != 0;
577666
ws->frame_state.opcode = (*data_ptr & 0x0F);
667+
ws->frame_state.compressed = (*data_ptr & 0x40) != 0; // RSV1 bit in the header
578668
data_ptr ++;
579669
mask = ((*data_ptr >> 7) & 0x01);
580670
payload_len = (*data_ptr & 0x7F);
@@ -979,14 +1069,65 @@ esp_err_t esp_transport_ws_set_config(esp_transport_handle_t t, const esp_transp
9791069
}
9801070

9811071
ws->propagate_control_frames = config->propagate_control_frames;
1072+
ws->per_msg_compress = config->per_msg_compress;
1073+
ws->per_msg_client_no_ctx_takeover = config->per_msg_client_no_ctx_takeover;
1074+
ws->per_msg_server_no_ctx_takeover = config->per_msg_server_no_ctx_takeover;
1075+
1076+
if (config->per_msg_client_deflate_window_bit < 8 || config->per_msg_client_deflate_window_bit > 15) {
1077+
ws->per_msg_client_deflate_window_bit = 0;
1078+
} else {
1079+
ws->per_msg_client_deflate_window_bit = config->per_msg_client_deflate_window_bit;
1080+
}
1081+
1082+
if (config->per_msg_server_deflate_window_bit < 8 || config->per_msg_server_deflate_window_bit > 15) {
1083+
ws->per_msg_server_deflate_window_bit = 0;
1084+
} else {
1085+
ws->per_msg_server_deflate_window_bit = config->per_msg_server_deflate_window_bit;
1086+
}
9821087

9831088
return err;
9841089
}
9851090

9861091
bool esp_transport_ws_get_fin_flag(esp_transport_handle_t t)
9871092
{
988-
transport_ws_t *ws = esp_transport_get_context_data(t);
989-
return ws->frame_state.fin;
1093+
transport_ws_t *ws = esp_transport_get_context_data(t);
1094+
return ws->frame_state.fin;
1095+
}
1096+
1097+
bool esp_transport_ws_get_rsv1_flag(esp_transport_handle_t t)
1098+
{
1099+
transport_ws_t *ws = esp_transport_get_context_data(t);
1100+
return ws->frame_state.compressed;
1101+
}
1102+
1103+
bool esp_transport_ws_get_per_msg_compress(esp_transport_handle_t t)
1104+
{
1105+
transport_ws_t *ws = esp_transport_get_context_data(t);
1106+
return ws->per_msg_compress;
1107+
}
1108+
1109+
int esp_transport_ws_get_per_msg_client_deflate_window_bit(esp_transport_handle_t t)
1110+
{
1111+
transport_ws_t *ws = esp_transport_get_context_data(t);
1112+
return ws->per_msg_client_deflate_window_bit;
1113+
}
1114+
1115+
int esp_transport_ws_get_per_msg_server_deflate_window_bit(esp_transport_handle_t t)
1116+
{
1117+
transport_ws_t *ws = esp_transport_get_context_data(t);
1118+
return ws->per_msg_server_deflate_window_bit;
1119+
}
1120+
1121+
bool esp_transport_ws_get_per_msg_server_no_ctx_takeover(esp_transport_handle_t t)
1122+
{
1123+
transport_ws_t *ws = esp_transport_get_context_data(t);
1124+
return ws->per_msg_server_no_ctx_takeover && ws->per_msg_compress;
1125+
}
1126+
1127+
bool esp_transport_ws_get_per_msg_client_no_ctx_takeover(esp_transport_handle_t t)
1128+
{
1129+
transport_ws_t *ws = esp_transport_get_context_data(t);
1130+
return ws->per_msg_client_no_ctx_takeover && ws->per_msg_compress;
9901131
}
9911132

9921133
int esp_transport_ws_get_upgrade_request_status(esp_transport_handle_t t)

0 commit comments

Comments
 (0)