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
8087static 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 );
8192static 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. */
464588static 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 );
0 commit comments