Skip to content

Commit 44267fe

Browse files
authored
Validate websocket handshake response (#410)
**Issue:** The websocket client did not fully validate the server's response, as specified in [RFC-6455 Section 4.1](https://www.rfc-editor.org/rfc/rfc6455#section-4.1) The client was doing 1/6 required steps (checking the status-code, but not checking any headers) **Description of changes:** Validate the "Upgrade", "Connection", and "Sec-WebSocket-Accept" headers. Now we're doing 4/6 required steps. Still need to check "Sec-WebSocket-Extensions" and "Sec-WebSocket-Protocol". That will be in a followup PR.
1 parent 2c845b0 commit 44267fe

File tree

5 files changed

+378
-74
lines changed

5 files changed

+378
-74
lines changed

include/aws/http/websocket.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,14 @@ struct aws_websocket_client_connection_options {
245245

246246
/**
247247
* Called repeatedly as payload data arrives.
248-
* Required if `on_incoming_frame_begin` is set.
248+
* Optional.
249249
* See `aws_websocket_on_incoming_frame_payload_fn`.
250250
*/
251251
aws_websocket_on_incoming_frame_payload_fn *on_incoming_frame_payload;
252252

253253
/**
254254
* Called when done processing an incoming frame.
255-
* Required if `on_incoming_frame_begin` is set.
255+
* Optional.
256256
* See `aws_websocket_on_incoming_frame_complete_fn`.
257257
*/
258258
aws_websocket_on_incoming_frame_complete_fn *on_incoming_frame_complete;

source/websocket.c

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,16 +1373,14 @@ static int s_decoder_on_payload(struct aws_byte_cursor data, void *user_data) {
13731373

13741374
/* Invoke user cb */
13751375
static int s_decoder_on_user_payload(struct aws_websocket *websocket, struct aws_byte_cursor data) {
1376-
if (!websocket->on_incoming_frame_payload) {
1377-
return AWS_OP_SUCCESS;
1378-
}
1376+
if (websocket->on_incoming_frame_payload) {
1377+
if (!websocket->on_incoming_frame_payload(
1378+
websocket, websocket->thread_data.current_incoming_frame, data, websocket->user_data)) {
13791379

1380-
if (!websocket->on_incoming_frame_payload(
1381-
websocket, websocket->thread_data.current_incoming_frame, data, websocket->user_data)) {
1382-
1383-
AWS_LOGF_ERROR(
1384-
AWS_LS_HTTP_WEBSOCKET, "id=%p: Incoming payload callback has reported a failure.", (void *)websocket);
1385-
return aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
1380+
AWS_LOGF_ERROR(
1381+
AWS_LS_HTTP_WEBSOCKET, "id=%p: Incoming payload callback has reported a failure.", (void *)websocket);
1382+
return aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
1383+
}
13861384
}
13871385

13881386
/* If this is a "data" frame's payload, let the window shrink */

source/websocket_bootstrap.c

Lines changed: 180 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
* SPDX-License-Identifier: Apache-2.0.
44
*/
55

6+
#include <aws/cal/hash.h>
7+
#include <aws/common/encoding.h>
68
#include <aws/common/logging.h>
79
#include <aws/http/connection.h>
810
#include <aws/http/private/http_impl.h>
11+
#include <aws/http/private/strutil.h>
912
#include <aws/http/private/websocket_impl.h>
1013
#include <aws/http/request_response.h>
1114
#include <aws/http/status_code.h>
@@ -66,6 +69,10 @@ struct aws_websocket_client_bootstrap {
6669
/* Handshake request data */
6770
struct aws_http_message *handshake_request;
6871

72+
/* Given the "Sec-WebSocket-Key" from the request,
73+
* this is what we expect the response's "Sec-WebSocket-Accept" to be */
74+
struct aws_byte_buf expected_sec_websocket_accept;
75+
6976
/* Handshake response data */
7077
int response_status;
7178
struct aws_http_headers *response_headers;
@@ -78,6 +85,10 @@ struct aws_websocket_client_bootstrap {
7885
};
7986

8087
static void s_ws_bootstrap_destroy(struct aws_websocket_client_bootstrap *ws_bootstrap);
88+
static int s_ws_bootstrap_calculate_sec_websocket_accept(
89+
struct aws_byte_cursor sec_websocket_key,
90+
struct aws_byte_buf *out_buf,
91+
struct aws_allocator *alloc);
8192
static void s_ws_bootstrap_cancel_setup_due_to_err(
8293
struct aws_websocket_client_bootstrap *ws_bootstrap,
8394
struct aws_http_connection *http_connection,
@@ -125,24 +136,19 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op
125136
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
126137
}
127138

128-
bool all_frame_callbacks_set =
129-
options->on_incoming_frame_begin && options->on_incoming_frame_payload && options->on_incoming_frame_begin;
130-
131-
bool no_frame_callbacks_set =
132-
!options->on_incoming_frame_begin && !options->on_incoming_frame_payload && !options->on_incoming_frame_begin;
133-
134-
if (!(all_frame_callbacks_set || no_frame_callbacks_set)) {
139+
if (!options->handshake_request) {
135140
AWS_LOGF_ERROR(
136141
AWS_LS_HTTP_WEBSOCKET_SETUP,
137-
"id=static: Invalid websocket connection options,"
138-
" either all frame-handling callbacks must be set, or none must be set.");
142+
"id=static: Invalid connection options, missing required request for websocket client handshake.");
139143
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
140144
}
141145

142-
if (!options->handshake_request) {
146+
const struct aws_http_headers *request_headers = aws_http_message_get_headers(options->handshake_request);
147+
struct aws_byte_cursor sec_websocket_key;
148+
if (aws_http_headers_get(request_headers, aws_byte_cursor_from_c_str("Sec-WebSocket-Key"), &sec_websocket_key)) {
143149
AWS_LOGF_ERROR(
144150
AWS_LS_HTTP_WEBSOCKET_SETUP,
145-
"id=static: Invalid connection options, missing required request for websocket client handshake.");
151+
"id=static: Websocket handshake request is missing required 'Sec-WebSocket-Key' header");
146152
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
147153
}
148154

@@ -164,6 +170,11 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op
164170
ws_bootstrap->response_headers = aws_http_headers_new(ws_bootstrap->alloc);
165171
aws_byte_buf_init(&ws_bootstrap->response_body, ws_bootstrap->alloc, 0);
166172

173+
if (s_ws_bootstrap_calculate_sec_websocket_accept(
174+
sec_websocket_key, &ws_bootstrap->expected_sec_websocket_accept, ws_bootstrap->alloc)) {
175+
goto error;
176+
}
177+
167178
/* Initiate HTTP connection */
168179
struct aws_http_client_connection_options http_options = AWS_HTTP_CLIENT_CONNECTION_OPTIONS_INIT;
169180
http_options.allocator = ws_bootstrap->alloc;
@@ -203,7 +214,7 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op
203214
"id=static: Websocket failed to initiate HTTP connection, error %d (%s)",
204215
aws_last_error(),
205216
aws_error_name(aws_last_error()));
206-
goto error_already_logged;
217+
goto error;
207218
}
208219

209220
/* Success! (so far) */
@@ -217,7 +228,7 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op
217228

218229
return AWS_OP_SUCCESS;
219230

220-
error_already_logged:
231+
error:
221232
s_ws_bootstrap_destroy(ws_bootstrap);
222233
return AWS_OP_ERR;
223234
}
@@ -229,11 +240,96 @@ static void s_ws_bootstrap_destroy(struct aws_websocket_client_bootstrap *ws_boo
229240

230241
aws_http_message_release(ws_bootstrap->handshake_request);
231242
aws_http_headers_release(ws_bootstrap->response_headers);
243+
aws_byte_buf_clean_up(&ws_bootstrap->expected_sec_websocket_accept);
232244
aws_byte_buf_clean_up(&ws_bootstrap->response_body);
233245

234246
aws_mem_release(ws_bootstrap->alloc, ws_bootstrap);
235247
}
236248

249+
/* Given the handshake request's "Sec-WebSocket-Key" value,
250+
* calculate the expected value for the response's "Sec-WebSocket-Accept".
251+
* RFC-6455 Section 4.1:
252+
* base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-Key|
253+
* (as a string, not base64-decoded) with the string
254+
* "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
255+
* trailing whitespace
256+
*/
257+
static int s_ws_bootstrap_calculate_sec_websocket_accept(
258+
struct aws_byte_cursor sec_websocket_key,
259+
struct aws_byte_buf *out_buf,
260+
struct aws_allocator *alloc) {
261+
262+
AWS_ASSERT(out_buf && !out_buf->allocator && out_buf->len == 0); /* expect buf to be uninitialized */
263+
264+
/* note: leading and trailing whitespace was already trimmed by aws_http_headers */
265+
266+
/* optimization: skip concatenating Sec-WebSocket-Key and the magic string.
267+
* just run the SHA1 over the first string, and then the 2nd. */
268+
269+
bool success = false;
270+
struct aws_byte_cursor magic_string = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
271+
272+
/* SHA-1 */
273+
struct aws_hash *sha1 = aws_sha1_new(alloc);
274+
if (!sha1) {
275+
AWS_LOGF_ERROR(
276+
AWS_LS_HTTP_WEBSOCKET_SETUP,
277+
"id=static: Failed to initiate SHA1, error %d (%s)",
278+
aws_last_error(),
279+
aws_error_name(aws_last_error()));
280+
goto cleanup;
281+
}
282+
283+
if (aws_hash_update(sha1, &sec_websocket_key) || aws_hash_update(sha1, &magic_string)) {
284+
AWS_LOGF_ERROR(
285+
AWS_LS_HTTP_WEBSOCKET_SETUP,
286+
"id=static: Failed to update SHA1, error %d (%s)",
287+
aws_last_error(),
288+
aws_error_name(aws_last_error()));
289+
goto cleanup;
290+
}
291+
292+
uint8_t sha1_storage[AWS_SHA1_LEN];
293+
struct aws_byte_buf sha1_buf = aws_byte_buf_from_empty_array(sha1_storage, sizeof(sha1_storage));
294+
if (aws_hash_finalize(sha1, &sha1_buf, 0)) {
295+
AWS_LOGF_ERROR(
296+
AWS_LS_HTTP_WEBSOCKET_SETUP,
297+
"id=static: Failed to finalize SHA1, error %d (%s)",
298+
aws_last_error(),
299+
aws_error_name(aws_last_error()));
300+
goto cleanup;
301+
}
302+
303+
/* base64-encoded SHA-1 (clear out_buf, and write to it again) */
304+
size_t base64_encode_sha1_len;
305+
if (aws_base64_compute_encoded_len(sha1_buf.len, &base64_encode_sha1_len)) {
306+
AWS_LOGF_ERROR(
307+
AWS_LS_HTTP_WEBSOCKET_SETUP,
308+
"id=static: Failed to determine Base64-encoded length, error %d (%s)",
309+
aws_last_error(),
310+
aws_error_name(aws_last_error()));
311+
goto cleanup;
312+
}
313+
aws_byte_buf_init(out_buf, alloc, base64_encode_sha1_len);
314+
315+
struct aws_byte_cursor sha1_cursor = aws_byte_cursor_from_buf(&sha1_buf);
316+
if (aws_base64_encode(&sha1_cursor, out_buf)) {
317+
AWS_LOGF_ERROR(
318+
AWS_LS_HTTP_WEBSOCKET_SETUP,
319+
"id=static: Failed to Base64-encode, error %d (%s)",
320+
aws_last_error(),
321+
aws_error_name(aws_last_error()));
322+
goto cleanup;
323+
}
324+
325+
success = true;
326+
cleanup:
327+
if (sha1) {
328+
aws_hash_destroy(sha1);
329+
}
330+
return success ? AWS_OP_SUCCESS : AWS_OP_ERR;
331+
}
332+
237333
/* Called if something goes wrong after an HTTP connection is established.
238334
* The HTTP connection is closed.
239335
* We must wait for its shutdown to complete before informing user of the failed websocket setup. */
@@ -457,15 +553,83 @@ static int s_ws_bootstrap_on_handshake_response_headers(
457553
return AWS_OP_SUCCESS;
458554
}
459555

556+
static int s_ws_bootstrap_validate_header(
557+
struct aws_websocket_client_bootstrap *ws_bootstrap,
558+
const char *name,
559+
struct aws_byte_cursor expected_value,
560+
bool case_sensitive) {
561+
562+
struct aws_byte_cursor actual_value;
563+
if (aws_http_headers_get(ws_bootstrap->response_headers, aws_byte_cursor_from_c_str(name), &actual_value)) {
564+
AWS_LOGF_ERROR(
565+
AWS_LS_HTTP_WEBSOCKET_SETUP, "id=%p: Response lacks required '%s' header", (void *)ws_bootstrap, name);
566+
return aws_raise_error(AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE);
567+
}
568+
569+
bool matches = case_sensitive ? aws_byte_cursor_eq(&expected_value, &actual_value)
570+
: aws_byte_cursor_eq_ignore_case(&expected_value, &actual_value);
571+
if (!matches) {
572+
AWS_LOGF_ERROR(
573+
AWS_LS_HTTP_WEBSOCKET_SETUP,
574+
"id=%p: Response '%s' header has wrong value. Expected '" PRInSTR "'. Received '" PRInSTR "'",
575+
(void *)ws_bootstrap,
576+
name,
577+
AWS_BYTE_CURSOR_PRI(expected_value),
578+
AWS_BYTE_CURSOR_PRI(actual_value));
579+
return aws_raise_error(AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE);
580+
}
581+
582+
return AWS_OP_SUCCESS;
583+
}
584+
460585
/* OK, we've got all the headers for the 101 Switching Protocols response.
461-
* Verify handshake response according to RFC-6455 Section 1.3,
462-
* install the websocket handler into the channel,
586+
* Validate the handshake response, install the websocket handler into the channel,
463587
* and invoke the on_connection_setup callback. */
464588
static int s_ws_bootstrap_validate_response_and_install_websocket_handler(
465589
struct aws_websocket_client_bootstrap *ws_bootstrap,
466590
struct aws_http_connection *http_connection) {
467591

468-
/* TODO: validate Sec-WebSocket-Accept header */
592+
/* RFC-6455 Section 4.1 - The client MUST validate the server's response as follows... */
593+
594+
/* (we already checked step 1, that status code is 101) */
595+
AWS_FATAL_ASSERT(ws_bootstrap->response_status == AWS_HTTP_STATUS_CODE_101_SWITCHING_PROTOCOLS);
596+
597+
/* 2. If the response lacks an |Upgrade| header field or the |Upgrade|
598+
* header field contains a value that is not an ASCII case-
599+
* insensitive match for the value "websocket", the client MUST
600+
* _Fail the WebSocket Connection_. */
601+
if (s_ws_bootstrap_validate_header(
602+
ws_bootstrap, "Upgrade", aws_byte_cursor_from_c_str("websocket"), false /*case_sensitive*/)) {
603+
goto error;
604+
}
605+
606+
/* 3. If the response lacks a |Connection| header field or the
607+
* |Connection| header field doesn't contain a token that is an
608+
* ASCII case-insensitive match for the value "Upgrade", the client
609+
* MUST _Fail the WebSocket Connection_. */
610+
if (s_ws_bootstrap_validate_header(
611+
ws_bootstrap, "Connection", aws_byte_cursor_from_c_str("Upgrade"), false /*case_sensitive*/)) {
612+
goto error;
613+
}
614+
615+
/* 4. If the response lacks a |Sec-WebSocket-Accept| header field or
616+
* the |Sec-WebSocket-Accept| contains a value other than the
617+
* base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
618+
* Key| (as a string, not base64-decoded) with the string "258EAFA5-
619+
* E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
620+
* trailing whitespace, the client MUST _Fail the WebSocket
621+
* Connection_. */
622+
if (s_ws_bootstrap_validate_header(
623+
ws_bootstrap,
624+
"Sec-WebSocket-Accept",
625+
aws_byte_cursor_from_buf(&ws_bootstrap->expected_sec_websocket_accept),
626+
true /*case_sensitive*/)) {
627+
goto error;
628+
}
629+
630+
/* TODO: validate Sec-WebSocket-Extensions */
631+
632+
/* TODO: validate Sec-WebSocket-Protocol */
469633

470634
/* Insert websocket handler into channel */
471635
struct aws_channel *channel = s_system_vtable->aws_http_connection_get_channel(http_connection);

tests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ add_test_case(websocket_boot_report_unexpected_http_shutdown)
228228
add_test_case(websocket_boot_fail_from_handshake_rejection)
229229
add_test_case(websocket_boot_fail_before_handshake_rejection_body)
230230
add_test_case(websocket_boot_fail_before_handshake_rejection_stream_complete)
231+
add_test_case(websocket_boot_fail_from_invalid_upgrade_header)
232+
add_test_case(websocket_boot_fail_from_missing_upgrade_header)
233+
add_test_case(websocket_boot_fail_from_invalid_connection_header)
234+
add_test_case(websocket_boot_fail_from_invalid_sec_websocket_upgrade_header)
231235
add_test_case(websocket_handshake_key_max_length)
232236
add_test_case(websocket_handshake_key_randomness)
233237

0 commit comments

Comments
 (0)