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
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 );
8191static 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. */
464568static 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 );
0 commit comments