Skip to content

Commit 2f0bef0

Browse files
committed
validate "Upgrade" "Connection" and "Sec-WebSocket-Accept" headers
1 parent 2c845b0 commit 2f0bef0

File tree

4 files changed

+178
-27
lines changed

4 files changed

+178
-27
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: 160 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,9 @@ 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);
8191
static void s_ws_bootstrap_cancel_setup_due_to_err(
8292
struct aws_websocket_client_bootstrap *ws_bootstrap,
8393
struct aws_http_connection *http_connection,
@@ -125,24 +135,19 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op
125135
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
126136
}
127137

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)) {
138+
if (!options->handshake_request) {
135139
AWS_LOGF_ERROR(
136140
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.");
141+
"id=static: Invalid connection options, missing required request for websocket client handshake.");
139142
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
140143
}
141144

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

@@ -160,10 +165,16 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op
160165
ws_bootstrap->websocket_frame_payload_callback = options->on_incoming_frame_payload;
161166
ws_bootstrap->websocket_frame_complete_callback = options->on_incoming_frame_complete;
162167
ws_bootstrap->handshake_request = aws_http_message_acquire(options->handshake_request);
168+
aws_byte_buf_init(&ws_bootstrap->expected_sec_websocket_accept, ws_bootstrap->alloc, 0);
163169
ws_bootstrap->response_status = AWS_HTTP_STATUS_CODE_UNKNOWN;
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)) {
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,76 @@ 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+
261+
AWS_ASSERT(out_buf && out_buf->allocator && out_buf->len == 0); /* expect buf to be initialized empty */
262+
struct aws_allocator *alloc = out_buf->allocator;
263+
264+
/* ignore leading and trailing whitespace */
265+
sec_websocket_key = aws_strutil_trim_http_whitespace(sec_websocket_key);
266+
267+
/* concatenation of key with magic string (store temporarily in out_buf) */
268+
struct aws_byte_cursor magic_string = aws_byte_cursor_from_c_str("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
269+
270+
aws_byte_buf_reserve(out_buf, sec_websocket_key.len + magic_string.len);
271+
aws_byte_buf_append_dynamic(out_buf, &sec_websocket_key);
272+
aws_byte_buf_append_dynamic(out_buf, &magic_string);
273+
struct aws_byte_cursor key_and_magic_string = aws_byte_cursor_from_buf(out_buf);
274+
275+
/* SHA-1 */
276+
uint8_t sha1_storage[AWS_SHA1_LEN];
277+
struct aws_byte_buf sha1_buf = aws_byte_buf_from_empty_array(sha1_storage, sizeof(sha1_storage));
278+
if (aws_sha1_compute(alloc, &key_and_magic_string, &sha1_buf, 0)) {
279+
AWS_LOGF_ERROR(
280+
AWS_LS_HTTP_WEBSOCKET_SETUP,
281+
"id=static: Failed to compute SHA1, error %d (%s)",
282+
aws_last_error(),
283+
aws_error_name(aws_last_error()));
284+
return AWS_OP_ERR;
285+
}
286+
287+
/* base64-encoded SHA-1 (clear out_buf, and write to it again) */
288+
size_t base64_encode_sha1_len;
289+
if (aws_base64_compute_encoded_len(sha1_buf.len, &base64_encode_sha1_len)) {
290+
AWS_LOGF_ERROR(
291+
AWS_LS_HTTP_WEBSOCKET_SETUP,
292+
"id=static: Failed to determine Base64-encoded length, error %d (%s)",
293+
aws_last_error(),
294+
aws_error_name(aws_last_error()));
295+
return AWS_OP_ERR;
296+
}
297+
aws_byte_buf_reset(out_buf, false /*zero_contents*/);
298+
aws_byte_buf_reserve(out_buf, base64_encode_sha1_len);
299+
300+
struct aws_byte_cursor sha1_cursor = aws_byte_cursor_from_buf(&sha1_buf);
301+
if (aws_base64_encode(&sha1_cursor, out_buf)) {
302+
AWS_LOGF_ERROR(
303+
AWS_LS_HTTP_WEBSOCKET_SETUP,
304+
"id=static: Failed to Base64-encode, error %d (%s)",
305+
aws_last_error(),
306+
aws_error_name(aws_last_error()));
307+
return AWS_OP_ERR;
308+
}
309+
310+
return AWS_OP_SUCCESS;
311+
}
312+
237313
/* Called if something goes wrong after an HTTP connection is established.
238314
* The HTTP connection is closed.
239315
* We must wait for its shutdown to complete before informing user of the failed websocket setup. */
@@ -457,15 +533,83 @@ static int s_ws_bootstrap_on_handshake_response_headers(
457533
return AWS_OP_SUCCESS;
458534
}
459535

536+
static int s_ws_bootstrap_validate_header(
537+
struct aws_websocket_client_bootstrap *ws_bootstrap,
538+
const char *name,
539+
struct aws_byte_cursor expected_value,
540+
bool case_sensitive) {
541+
542+
struct aws_byte_cursor actual_value;
543+
if (aws_http_headers_get(ws_bootstrap->response_headers, aws_byte_cursor_from_c_str(name), &actual_value)) {
544+
AWS_LOGF_ERROR(
545+
AWS_LS_HTTP_WEBSOCKET_SETUP, "id=%p: Response lacks required '%s' header", (void *)ws_bootstrap, name);
546+
return aws_raise_error(AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE);
547+
}
548+
549+
bool matches = case_sensitive ? aws_byte_cursor_eq(&expected_value, &actual_value)
550+
: aws_byte_cursor_eq_ignore_case(&expected_value, &actual_value);
551+
if (!matches) {
552+
AWS_LOGF_ERROR(
553+
AWS_LS_HTTP_WEBSOCKET_SETUP,
554+
"id=%p: Response '%s' header has wrong value. Expected '" PRInSTR "'. Received '" PRInSTR "'",
555+
(void *)ws_bootstrap,
556+
name,
557+
AWS_BYTE_CURSOR_PRI(expected_value),
558+
AWS_BYTE_CURSOR_PRI(actual_value));
559+
return aws_raise_error(AWS_ERROR_HTTP_WEBSOCKET_UPGRADE_FAILURE);
560+
}
561+
562+
return AWS_OP_SUCCESS;
563+
}
564+
460565
/* 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,
566+
* Validate the handshake response, install the websocket handler into the channel,
463567
* and invoke the on_connection_setup callback. */
464568
static int s_ws_bootstrap_validate_response_and_install_websocket_handler(
465569
struct aws_websocket_client_bootstrap *ws_bootstrap,
466570
struct aws_http_connection *http_connection) {
467571

468-
/* TODO: validate Sec-WebSocket-Accept header */
572+
/* RFC-6455 Section 4.1 - The client MUST validate the server's response as follows... */
573+
574+
/* (we already checked step 1, that status code is 101) */
575+
AWS_FATAL_ASSERT(ws_bootstrap->response_status == AWS_HTTP_STATUS_CODE_101_SWITCHING_PROTOCOLS);
576+
577+
/* 2. If the response lacks an |Upgrade| header field or the |Upgrade|
578+
* header field contains a value that is not an ASCII case-
579+
* insensitive match for the value "websocket", the client MUST
580+
* _Fail the WebSocket Connection_. */
581+
if (s_ws_bootstrap_validate_header(
582+
ws_bootstrap, "Upgrade", aws_byte_cursor_from_c_str("websocket"), false /*case_sensitive*/)) {
583+
goto error;
584+
}
585+
586+
/* 3. If the response lacks a |Connection| header field or the
587+
* |Connection| header field doesn't contain a token that is an
588+
* ASCII case-insensitive match for the value "Upgrade", the client
589+
* MUST _Fail the WebSocket Connection_. */
590+
if (s_ws_bootstrap_validate_header(
591+
ws_bootstrap, "Connection", aws_byte_cursor_from_c_str("Upgrade"), false /*case_sensitive*/)) {
592+
goto error;
593+
}
594+
595+
/* 4. If the response lacks a |Sec-WebSocket-Accept| header field or
596+
* the |Sec-WebSocket-Accept| contains a value other than the
597+
* base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
598+
* Key| (as a string, not base64-decoded) with the string "258EAFA5-
599+
* E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
600+
* trailing whitespace, the client MUST _Fail the WebSocket
601+
* Connection_. */
602+
if (s_ws_bootstrap_validate_header(
603+
ws_bootstrap,
604+
"Sec-WebSocket-Accept",
605+
aws_byte_cursor_from_buf(&ws_bootstrap->expected_sec_websocket_accept),
606+
true /*case_sensitive*/)) {
607+
goto error;
608+
}
609+
610+
/* TODO: validate Sec-WebSocket-Extensions */
611+
612+
/* TODO: validate Sec-WebSocket-Protocol */
469613

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

tests/test_websocket_bootstrap.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ static const struct aws_websocket_client_bootstrap_system_vtable s_mock_system_v
4747
.aws_websocket_handler_new = s_mock_websocket_handler_new,
4848
};
4949

50+
/* Hardcoded value for "Sec-WebSocket-Key" header in handshake request. */
51+
static const char *s_sec_websocket_key_value = "dGhlIHNhbXBsZSBub25jZQ==";
52+
5053
static const struct aws_http_header s_accepted_response_headers[] = {
5154
{
5255
.name = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("Upgrade"),
@@ -437,6 +440,12 @@ static int s_drive_websocket_connect(int *out_error_code) {
437440
goto finishing_checks;
438441
}
439442

443+
struct aws_http_headers *request_headers = aws_http_message_get_headers(s_tester.handshake_request);
444+
ASSERT_SUCCESS(aws_http_headers_set(
445+
request_headers,
446+
aws_byte_cursor_from_c_str("Sec-WebSocket-Key"),
447+
aws_byte_cursor_from_c_str(s_sec_websocket_key_value)));
448+
440449
struct aws_websocket_client_connection_options ws_options = {
441450
.allocator = s_tester.alloc,
442451
.bootstrap = (void *)"client channel bootstrap",

0 commit comments

Comments
 (0)