From 897f64e4feb7014872b1e1244a39366f4c41c7b7 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Thu, 13 Oct 2016 16:25:11 +0200 Subject: [PATCH] Reuse http_network library. Reintroduced HTTPD_STREAM_SOCKET for backward compatibility, and ease of usage. Added websocket libraries (client, and protocol). --- .../http_network/src/http_stream_socket.e | 8 + .../src/no_ssl/http_stream_ssl_socket.e | 12 + .../src/ssl/http_stream_ssl_socket.e | 53 +- .../client/src/no_ssl/web_socket_client.e | 29 + .../client/src/ssl/web_socket_client.e | 54 ++ .../network/websocket/client/src/web_socket.e | 100 +++ .../client/src/web_socket_client_i.e | 604 ++++++++++++++ .../client/src/web_socket_handshake_data.e | 90 +++ .../websocket/client/src/web_socket_impl.e | 739 ++++++++++++++++++ .../client/src/web_socket_null_client.e | 30 + .../client/src/web_socket_ready_state.e | 120 +++ .../client/src/web_socket_subscriber.e | 61 ++ .../client/web_socket_client-safe.ecf | 45 ++ .../websocket/client/web_socket_client.ecf | 64 ++ .../websocket/protocol/web_socket_constants.e | 0 .../protocol/web_socket_error_frame.e | 0 .../websocket/protocol/web_socket_frame.e | 0 .../protocol/web_socket_protocol-safe.ecf | 8 + .../protocol/web_socket_protocol.ecf | 8 + .../connectors/standalone/standalone-safe.ecf | 1 - .../connectors/standalone/standalone.ecf | 1 - .../none/httpd_connection_handler.e | 4 +- .../scoop/httpd_connection_handler.e | 10 +- .../concurrency/scoop/httpd_request_handler.e | 4 +- .../scoop/httpd_request_handler_factory.e | 2 +- .../concurrency/scoop/pool/concurrent_pool.e | 2 +- .../scoop/pool/concurrent_pool_factory.e | 2 +- .../scoop/pool/concurrent_pool_item.e | 2 +- .../thread/httpd_connection_handler.e | 4 +- .../configuration/httpd_configuration_i.e | 2 +- .../httpd/configuration/httpd_constants.e | 10 + .../configuration/httpd_request_settings.e | 12 +- library/server/httpd/httpd-safe.ecf | 1 - library/server/httpd/httpd.ecf | 1 - .../server/httpd/httpd_connection_handler_i.e | 4 +- library/server/httpd/httpd_controller.e | 9 +- library/server/httpd/httpd_debug_facilities.e | 2 +- library/server/httpd/httpd_logger.e | 2 +- library/server/httpd/httpd_logger_constants.e | 10 + .../httpd/httpd_request_handler_factory_i.e | 2 +- .../server/httpd/httpd_request_handler_i.e | 18 +- library/server/httpd/httpd_server_i.e | 6 +- library/server/httpd/httpd_server_observer.e | 10 + library/server/httpd/license.lic | 2 +- .../httpd/network/httpd_socket_factory.e | 30 + .../httpd/network/httpd_stream_socket.e | 32 + .../httpd/network/httpd_stream_ssl_socket.e | 51 ++ .../server/httpd/no_ssl/httpd_configuration.e | 2 +- library/server/httpd/no_ssl/httpd_server.e | 2 +- .../httpd/no_ssl/httpd_socket_factory.e | 17 - library/server/httpd/ssl/httpd_server.e | 20 +- .../server/httpd/ssl/httpd_socket_factory.e | 20 - .../connector/standalone_websocket-safe.ecf | 3 +- .../wsf/connector/standalone_websocket.ecf | 3 +- 54 files changed, 2233 insertions(+), 95 deletions(-) create mode 100644 library/network/websocket/client/src/no_ssl/web_socket_client.e create mode 100644 library/network/websocket/client/src/ssl/web_socket_client.e create mode 100644 library/network/websocket/client/src/web_socket.e create mode 100644 library/network/websocket/client/src/web_socket_client_i.e create mode 100644 library/network/websocket/client/src/web_socket_handshake_data.e create mode 100644 library/network/websocket/client/src/web_socket_impl.e create mode 100644 library/network/websocket/client/src/web_socket_null_client.e create mode 100644 library/network/websocket/client/src/web_socket_ready_state.e create mode 100644 library/network/websocket/client/src/web_socket_subscriber.e create mode 100644 library/network/websocket/client/web_socket_client-safe.ecf create mode 100644 library/network/websocket/client/web_socket_client.ecf rename library/{server/wsf/connector/standalone_websocket => network}/websocket/protocol/web_socket_constants.e (100%) rename library/{server/wsf/connector/standalone_websocket => network}/websocket/protocol/web_socket_error_frame.e (100%) rename library/{server/wsf/connector/standalone_websocket => network}/websocket/protocol/web_socket_frame.e (100%) create mode 100644 library/network/websocket/protocol/web_socket_protocol-safe.ecf create mode 100644 library/network/websocket/protocol/web_socket_protocol.ecf create mode 100644 library/server/httpd/network/httpd_socket_factory.e create mode 100644 library/server/httpd/network/httpd_stream_socket.e create mode 100644 library/server/httpd/network/httpd_stream_ssl_socket.e delete mode 100644 library/server/httpd/no_ssl/httpd_socket_factory.e delete mode 100644 library/server/httpd/ssl/httpd_socket_factory.e diff --git a/library/network/http_network/src/http_stream_socket.e b/library/network/http_network/src/http_stream_socket.e index 163af388..4ab0408a 100644 --- a/library/network/http_network/src/http_stream_socket.e +++ b/library/network/http_network/src/http_stream_socket.e @@ -19,6 +19,14 @@ create create {NETWORK_STREAM_SOCKET} make_from_descriptor_and_address +feature -- Status report + + is_ssl_supported: BOOLEAN + -- SSL supported? + once + Result := False + end + feature -- Input read_character_noexception diff --git a/library/network/http_network/src/no_ssl/http_stream_ssl_socket.e b/library/network/http_network/src/no_ssl/http_stream_ssl_socket.e index 3430ccc8..f69f1a89 100644 --- a/library/network/http_network/src/no_ssl/http_stream_ssl_socket.e +++ b/library/network/http_network/src/no_ssl/http_stream_ssl_socket.e @@ -20,6 +20,18 @@ create create {HTTP_STREAM_SSL_SOCKET} make_from_descriptor_and_address +feature -- Element change + + set_certificate_file_path (a_crt_filename: PATH) + do + end + + set_key_file_path (a_key_filename: PATH) + do + end + +invariant + ssl_not_supported: not is_ssl_supported -- Current is a Fake SSL interface! note copyright: "2011-2013, Javier Velilla, Jocelyn Fiat and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" diff --git a/library/network/http_network/src/ssl/http_stream_ssl_socket.e b/library/network/http_network/src/ssl/http_stream_ssl_socket.e index 408ced76..45fc71e5 100644 --- a/library/network/http_network/src/ssl/http_stream_ssl_socket.e +++ b/library/network/http_network/src/ssl/http_stream_ssl_socket.e @@ -20,6 +20,7 @@ inherit connect, shutdown, do_accept redefine + is_ssl_supported, put_managed_pointer, read_stream_noexception, read_into_pointer_noexception, @@ -41,6 +42,46 @@ create create {SSL_NETWORK_STREAM_SOCKET} make_from_descriptor_and_address +feature -- Status report + + is_ssl_supported: BOOLEAN + -- SSL supported? + once + Result := True + end + +feature -- SSL Helpers + + set_ssl_protocol_to_ssl_2_or_3 + -- Set `ssl_protocol' with `Ssl_23'. + do + set_tls_protocol ({SSL_PROTOCOL}.Ssl_23) + end + + set_ssl_protocol_to_tls_1_0 + -- Set `ssl_protocol' with `Tls_1_0'. + do + set_tls_protocol ({SSL_PROTOCOL}.Tls_1_0) + end + + set_ssl_protocol_to_tls_1_1 + -- Set `ssl_protocol' with `Tls_1_1'. + do + set_tls_protocol ({SSL_PROTOCOL}.Tls_1_1) + end + + set_ssl_protocol_to_tls_1_2 + -- Set `ssl_protocol' with `Tls_1_2'. + do + set_tls_protocol ({SSL_PROTOCOL}.Tls_1_2) + end + + set_ssl_protocol_to_dtls_1_0 + -- Set `ssl_protocol' with `Dtls_1_0'. + do + set_tls_protocol ({SSL_PROTOCOL}.Dtls_1_0) + end + feature -- Input read_stream_noexception (nb_char: INTEGER) @@ -130,18 +171,6 @@ feature -- Output end end -feature -- Element change - - set_certificate_filenames (a_crt_filename, a_key_filename: detachable READABLE_STRING_GENERAL) - do - if a_crt_filename /= Void then - set_certificate_file_name (a_crt_filename) - end - if a_key_filename /= Void then - set_key_file_name (a_key_filename) - end - end - note copyright: "2011-2013, Javier Velilla, Jocelyn Fiat and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" diff --git a/library/network/websocket/client/src/no_ssl/web_socket_client.e b/library/network/websocket/client/src/no_ssl/web_socket_client.e new file mode 100644 index 00000000..9fa65e66 --- /dev/null +++ b/library/network/websocket/client/src/no_ssl/web_socket_client.e @@ -0,0 +1,29 @@ +note + description: "[ + API to perform actions like opening and closing the connection, sending and receiving messages, and listening + for events triggered by the server + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEB_SOCKET_CLIENT + +inherit + WEB_SOCKET_CLIENT_I + +feature -- Status report + + is_ssl_supported: BOOLEAN = False + +feature -- Factory + + new_socket (a_port: INTEGER; a_host: STRING): HTTP_STREAM_SOCKET + do + if is_tunneled then + check ssl_supported: False end + end + create {HTTP_STREAM_SOCKET} Result.make_client_by_port (a_port, a_host) + end + +end diff --git a/library/network/websocket/client/src/ssl/web_socket_client.e b/library/network/websocket/client/src/ssl/web_socket_client.e new file mode 100644 index 00000000..13784ae2 --- /dev/null +++ b/library/network/websocket/client/src/ssl/web_socket_client.e @@ -0,0 +1,54 @@ +note + description: "[ + API to perform actions like opening and closing the connection, sending and receiving messages, and listening + for events triggered by the server + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEB_SOCKET_CLIENT + +inherit + WEB_SOCKET_CLIENT_I + +feature -- Status report + + is_ssl_supported: BOOLEAN = True + +feature -- Factory + + new_socket (a_port: INTEGER; a_host: STRING): HTTP_STREAM_SOCKET + local + l_ssl: HTTP_STREAM_SSL_SOCKET + do + if is_tunneled then + create l_ssl.make_client_by_port (a_port, a_host) + Result := l_ssl + if attached ssl_protocol as l_prot then + if l_prot.is_case_insensitive_equal ("ssl_2_3") then + l_ssl.set_ssl_protocol_to_ssl_2_or_3 + elseif l_prot.is_case_insensitive_equal ("tls_1_0") then + l_ssl.set_ssl_protocol_to_tls_1_0 + elseif l_prot.is_case_insensitive_equal ("tls_1_1") then + l_ssl.set_ssl_protocol_to_tls_1_1 + elseif l_prot.is_case_insensitive_equal ("tls_1_2") then + l_ssl.set_ssl_protocol_to_tls_1_2 + elseif l_prot.is_case_insensitive_equal ("dtls_1_0") then + l_ssl.set_ssl_protocol_to_dtls_1_0 + else -- Default + l_ssl.set_ssl_protocol_to_tls_1_2 + end + end + if attached ssl_key_file as k then + l_ssl.set_key_file_path (k) + end + if attached ssl_certificate_file as c then + l_ssl.set_certificate_file_path (c) + end + else + create {HTTP_STREAM_SOCKET} Result.make_client_by_port (a_port, a_host) + end + end + +end diff --git a/library/network/websocket/client/src/web_socket.e b/library/network/websocket/client/src/web_socket.e new file mode 100644 index 00000000..a3d7ed26 --- /dev/null +++ b/library/network/websocket/client/src/web_socket.e @@ -0,0 +1,100 @@ +note + description: "[ + API to perform actions like opening and closing the connection, sending and receiving messages, and listening + for events triggered by the server + ]" + author: "" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEB_SOCKET + +inherit + + WEB_SOCKET_CONSTANTS + +feature -- Access + + uri: READABLE_STRING_GENERAL + -- WebSocket Protocol defines two URI schemes, ws and wss for + -- unencrypted and encrypted traffic between the client and the server. + + port: INTEGER + -- Current WebSocket protocol. + + ws_port_default: INTEGER = 80 + -- WebSocket Protocol uses port 80 for regular WebSocket connections. + + wss_port_default: INTEGER = 443 + -- WebSocket connections tunneled over Transport Layer Security (TLS) use port 443. + + protocols: detachable LIST [STRING] + -- List of protocol names that the client supports. + + protocol: STRING + -- Client-Server protocol selected. + -- Has the result fo protocol negotiation between client and the server + -- By default it's an empty string. + + is_tunneled: BOOLEAN + -- Is the current connection tunneled over TLS/SSL? + local + l_uri: STRING + do + l_uri := uri.as_lower.as_string_8 + Result := l_uri.starts_with ("wss") -- TODO extract to ws_constants. + end + + ready_state: WEB_SOCKET_READY_STATE + -- Connection state + +feature -- Methods + + send (a_message: STRING) + -- Send a message `a_message' + require + is_open: ready_state.is_open + deferred + end + + close (a_id: INTEGER) + -- Close a websocket connection with a close id : `a_id' + deferred + ensure + is_closed: ready_state.is_closed + end + + close_with_description (a_id: INTEGER; a_description: READABLE_STRING_GENERAL) + -- Close a websocket connection with a close id : `a_id' and a description `a_description' + deferred + ensure + is_closed: ready_state.is_closed + end + +feature -- Change Element + + set_protocol (a_protocol: STRING) + -- Set `protocol' with `a_protocol' + do + protocol := a_protocol + ensure + protocol_set: protocol = a_protocol + end + + set_protocols (a_protocols: detachable ITERABLE [STRING]) + local + l_protocols: LIST [STRING] + do + if a_protocols /= Void then + create {ARRAYED_LIST [STRING]} l_protocols.make (0) + across + a_protocols as ic + loop + l_protocols.force (ic.item) + end + protocols := l_protocols + end + end + +end diff --git a/library/network/websocket/client/src/web_socket_client_i.e b/library/network/websocket/client/src/web_socket_client_i.e new file mode 100644 index 00000000..288ef06d --- /dev/null +++ b/library/network/websocket/client/src/web_socket_client_i.e @@ -0,0 +1,604 @@ +note + description: "[ + API to perform actions like opening and closing the connection, sending and receiving messages, and listening + for events triggered by the server + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEB_SOCKET_CLIENT_I + +inherit + + WEB_SOCKET_SUBSCRIBER + redefine + on_websocket_error, + on_websocket_text_message, + on_websocket_binary_message, + on_websocket_close, + on_websocket_open, + on_websocket_ping, + on_websocket_pong + end + + WEB_SOCKET + + THREAD + rename + make as thread_make + end + +feature -- Initialization + + initialize (a_uri: READABLE_STRING_GENERAL; a_protocols: detachable ITERABLE [STRING]) + -- Initialize websocket client + require + is_valid_uri: is_valid_uri (a_uri) + do + thread_make + uri := a_uri + set_default_port + create protocol.make_empty + set_protocols (a_protocols) + create ready_state.make + socket := new_socket (port, host) + create server_handshake.make + end + + initialize_with_port (a_uri: READABLE_STRING_GENERAL; a_port: INTEGER; a_protocols: detachable ITERABLE [STRING]) + -- Initialize websocket client + require + is_valid_uri: is_valid_uri (a_uri) + do + thread_make + uri := a_uri + port := a_port + create protocol.make_empty + set_protocols (a_protocols) + create ready_state.make + socket := new_socket (port, host) + create server_handshake.make + end + + initialize_with_host_port_and_path (a_host: READABLE_STRING_GENERAL; a_port: INTEGER; a_path: READABLE_STRING_GENERAL) + require + is_valid_uri: is_valid_uri (a_host) + do + thread_make + uri := a_host + ":" + a_port.out + a_path + port := a_port + create protocol.make_empty +-- set_protocols (a_protocols) + create ready_state.make + socket := new_socket (port, host) + create server_handshake.make + end + +feature -- Factory + + new_socket (a_port: INTEGER; a_host: STRING): HTTP_STREAM_SOCKET + -- New socket for port `a_port' on host `a_host'. + deferred + end + +feature -- Access + + socket: HTTP_STREAM_SOCKET + -- Socket + + has_error: BOOLEAN + do + Result := implementation.has_error + end + + is_server_hanshake_accepted: BOOLEAN + + is_valid_uri (a_uri: READABLE_STRING_GENERAL): BOOLEAN + -- Is `a_uri' a valid URI? + local + l_uri: URI + do + create l_uri.make_from_string (a_uri.as_string_8) + Result := l_uri.is_valid + end + + server_handshake: WEB_SOCKET_HANDSHAKE_DATA + -- Handshake data received from the server + +feature -- Access: ssl + + is_ssl_supported: BOOLEAN + -- Is SSL supported? + deferred + end + + ssl_protocol: detachable READABLE_STRING_GENERAL + -- SSL protocol , if `is_ssl_supported'. + + ssl_certificate_file: detachable PATH + -- SSL certificate file , if `is_ssl_supported'. + + ssl_key_file: detachable PATH + -- SSL key file , if `is_ssl_supported'. + +feature -- Element change + + set_ssl_protocol (a_prot: like ssl_protocol) + do + ssl_protocol := a_prot + end + + set_ssl_certificate_file (p: detachable PATH) + -- Set SSL certificate from file at `p'. + do + ssl_certificate_file := p + end + + set_ssl_key_file (p: detachable PATH) + -- Set SSL key from file at `p'. + do + ssl_key_file := p + end + +feature -- Events API + + on_open (a_message: STRING) + deferred + end + + on_text_message (a_message: STRING) + deferred + end + + on_binary_message (a_message: STRING) + deferred + end + + on_close (a_code: INTEGER; a_reason: STRING) + deferred + end + + on_error (a_error: STRING) + deferred + end + +feature -- Subscriber Events + + on_websocket_handshake (a_request: STRING) + -- Send handshake message + do + socket.put_string (a_request) + end + + on_websocket_text_message (a_message: STRING) + do + on_text_message (a_message) + end + + on_websocket_binary_message (a_message: STRING) + do + on_binary_message (a_message) + end + + on_websocket_open (a_message: STRING) + do + on_open (a_message) + end + + on_websocket_close (a_message: STRING) + do + close_with_description (1000, a_message) + on_close (1000, a_message) + end + + on_websocket_error (a_error: STRING) + do + on_error (a_error) + close_with_description (1002,"") + end + + on_websocket_ping (a_message: STRING) + do + do_send (10, a_message) + end + + on_websocket_pong (a_message: STRING) + do +-- do_send (9, a_message) + end + +feature -- Execute + + execute + require else + is_socket_valid: socket.exists + do + set_implementation + socket.connect + check + socket_connected: socket.is_connected + end + send_handshake + receive_handshake + if is_server_hanshake_accepted then + ready_state.set_state ({WEB_SOCKET_READY_STATE}.open) + on_websocket_open ("Open Connection") + from + until + ready_state.is_closed or has_error + loop + receive + end + else + on_websocket_error ("Server Handshake not accepted") + --log(Not connected) + socket.close + end + rescue + on_websocket_close ("") + socket.close + end + +feature -- Methods + + send (a_message: STRING) + do + do_send (1, a_message) + end + + send_binary (a_message: STRING) + do + do_send (2, a_message) + end + + close (a_id: INTEGER) + -- Close a websocket connection with a close id : `a_id' + do + do_send (8, "") + ready_state.set_state ({WEB_SOCKET_READY_STATE}.closed) + end + + close_with_description (a_id: INTEGER; a_description: READABLE_STRING_GENERAL) + -- Close a websocket connection with a close id : `a_id' and a description `a_description' + do + do_send (8, "") + ready_state.set_state ({WEB_SOCKET_READY_STATE}.closed) + end + +feature {NONE} -- Implementation + + set_implementation + do + create implementation.make_with_protocols_and_port (Current, host, protocols, port) + end + + send_handshake + local + l_uri: URI + l_handshake: STRING + l_random: SALT_XOR_SHIFT_64_GENERATOR + l_secure_protocol: STRING + do + create l_uri.make_from_string (uri.as_string_8) + create l_handshake.make_empty + if l_uri.path.is_empty then + l_handshake.append ("GET / HTTP/1.1") + l_handshake.append (crlf) + elseif l_uri.query = Void then + l_handshake.append ("GET " + l_uri.path + " HTTP/1.1") + l_handshake.append (crlf) + else + if attached l_uri.query as l_query then + l_handshake.append ("GET " + l_uri.path + "?" + l_query + " HTTP/1.1") + l_handshake.append (crlf) + end + end + if attached l_uri.host as l_host then + l_handshake.replace_substring_all ("$host", l_host) + l_handshake.append ("Host: " + l_host + ":" + port.out) + l_handshake.append (crlf) + end + l_handshake.append_string ("Upgrade: websocket") + l_handshake.append (crlf) + l_handshake.append_string ("Connection: Upgrade") + l_handshake.append (crlf) + l_handshake.append_string ("Sec-WebSocket-Key: ") + create l_random.make (16) + l_handshake.append_string (base64_encode_array (l_random.new_sequence)) + l_handshake.append (crlf) + if attached protocols as l_protocols then + create l_secure_protocol.make_empty + across + l_protocols as c + loop + l_secure_protocol.append (c.item) + l_secure_protocol.append (" ,") + end + l_secure_protocol.remove_tail (1) + l_handshake.append_string ("Sec-WebSocket-Protocol:" + l_secure_protocol) + l_handshake.append (crlf) + end + l_handshake.append_string ("Sec-WebSocket-Version: 13") + l_handshake.append (crlf) + l_handshake.append (crlf) + implementation.start_handshake (l_handshake) + end + + receive_handshake + do + analyze_request_message + if server_handshake.request_header.has_substring ("HTTP/1.1 101") and then attached server_handshake.request_header_map.item ("Upgrade") as l_upgrade_key and then -- Upgrade header must be present with value websocket + l_upgrade_key.is_case_insensitive_equal ("websocket") and then attached server_handshake.request_header_map.item ("Connection") as l_connection_key and then -- Connection header must be present with value Upgrade + l_connection_key.has_substring ("Upgrade") + then + is_server_hanshake_accepted := True + if attached server_handshake.request_header_map.item ("Sec-WebSocet-Protocol") as l_protocol then + set_protocol (l_protocol) + end + end + end + + receive + do + implementation.receive + end + + set_default_port + do + if is_tunneled then + port := wss_port_default + else + port := ws_port_default + end + end + + client_handshake_required_template: STRING = "[ + GET $resource HTTP/1.1 + Host: $host + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key: $key + Sec-WebSocket-Version: 13 + ]" + + base64_encode_array (a_sequence: ARRAY [NATURAL_8]): STRING_8 + -- Encode a byte array `a_sequence' into Base64 notation. + local + l_result: STRING + l_base_64: BASE64 + do + create l_result.make_empty + across + a_sequence as i + loop + l_result.append_character (i.item.to_character_8) + end + create l_base_64 + Result := l_base_64.encoded_string (l_result) + end + + host: STRING + local + l_uri: URI + do + create Result.make_empty + create l_uri.make_from_string (uri.as_string_8) + if attached l_uri.host as l_host then + Result := l_host + end + end + +feature -- Parse Request line + + analyze_request_message + -- Analyze message extracted from `socket' as HTTP request + require + input_readable: socket /= Void and then socket.is_open_read + local + end_of_stream: BOOLEAN + pos, n: INTEGER + line: detachable STRING + k, val: STRING + txt: STRING + do + create txt.make (64) + server_handshake.set_request_header (txt) + if attached next_line as l_request_line and then not l_request_line.is_empty then + txt.append (l_request_line) + txt.append_character ('%N') + else + server_handshake.mark_error + end + -- l_is_verbose := is_verbose + if not server_handshake.has_error then -- or l_is_verbose then + -- if `is_verbose' we can try to print the request, even if it is a bad HTTP request + from + line := next_line + until + line = Void or end_of_stream + loop + n := line.count + -- if l_is_verbose then + -- log (line) + -- end + pos := line.index_of (':', 1) + if pos > 0 then + k := line.substring (1, pos - 1) + if line [pos + 1].is_space then + pos := pos + 1 + end + if line [n] = '%R' then + n := n - 1 + end + val := line.substring (pos + 1, n) + server_handshake.put_header (k, val) + end + txt.append (line) + txt.append_character ('%N') + if line.is_empty or else line [1] = '%R' then + end_of_stream := True + else + line := next_line + end + end + end + end + + next_line: detachable STRING + -- Next line fetched from `socket' is available. + require + is_readable: socket.is_open_read + do + if socket.socket_ok then + socket.read_line_thread_aware + Result := socket.last_string + end + end + +feature -- {WEB_SOCKET_CLIENT} + +-- do_send (a_opcode: NATURAL_32; a_message: STRING) +-- local +-- l_chunks: INTEGER +-- i: INTEGER +-- l_index: INTEGER +-- l_chunk_size: INTEGER +-- l_key: STRING +-- l_message: STRING +-- l_frame : STRING +-- do +-- print ("%NMessage count:" + a_message.count.out) +-- create l_frame.make_empty +-- create l_message.make_empty +-- if a_message.count > 65535 then +-- --!Improve. this code need to be checked. +-- print("%N Case:1") +-- l_frame.append_code ((0x80 | a_opcode).to_natural_32) +-- l_frame.append_code ((0x80 | 127).to_natural_32) + +-- l_frame.append_code (0x0) +-- l_frame.append_code (0x0) +-- l_frame.append_code (0x0) +-- l_frame.append_code (0x0) +-- l_frame.append_code (((a_message.count) |>> 32).to_character_8.code.as_natural_32) +-- l_frame.append_code (((a_message.count) |>> 16).to_character_8.code.as_natural_32) +-- l_frame.append_code (((a_message.count ) |>> 8).to_character_8.code.as_natural_32) +-- l_frame.append_code ((a_message.count).to_character_8.code.as_natural_32) +-- elseif a_message.count > 125 then +-- print("%N Case:2") +-- print ("Message count:" + a_message.count.out) +-- l_frame.append_code ((0x80 | a_opcode).to_natural_32) +-- l_frame.append_code ((0x80 | 126).to_natural_32) +-- l_frame.append_code (((a_message.count ) |>> 8).as_natural_32) +-- l_frame.append_code ((a_message.count).to_character_8.code.as_natural_32) +-- print ("%NHeaderMessage:" + l_frame) +-- else +-- print("%N Case:3") +-- print ("Message count:" + a_message.count.out) +-- l_frame.append_code ((0x80 | a_opcode).to_natural_32) +-- l_frame.append_code ((0x80 | a_message.count).to_natural_32) +-- end +-- +-- l_key := new_key +-- l_frame.append (l_key.substring (1, 4)) +-- l_message := implementation.unmmask (a_message, l_key.substring (1, 4)) +-- l_frame.append (l_message) +-- socket.send_message (l_frame) +-- end + + do_send (a_opcode:INTEGER; a_message: READABLE_STRING_8) + local + i: INTEGER + l_chunk_size: INTEGER + l_chunk: READABLE_STRING_8 + l_header_message: STRING + l_message_count: INTEGER + n: NATURAL_64 + retried: BOOLEAN + l_message : STRING + l_key: STRING + do + if not retried then + create l_header_message.make_empty + l_header_message.append_code ((0x80 | a_opcode).to_natural_32) + l_message_count := a_message.count + n := l_message_count.to_natural_64 + if l_message_count > 0xffff then + --! Improve. this code needs to be checked. + l_header_message.append_code ((0x80 | 127).to_natural_32) + l_header_message.append_character ((n |>> 56).to_character_8) + l_header_message.append_character ((n |>> 48).to_character_8) + l_header_message.append_character ((n |>> 40).to_character_8) + l_header_message.append_character ((n |>> 32).to_character_8) + l_header_message.append_character ((n |>> 24).to_character_8) + l_header_message.append_character ((n |>> 16).to_character_8) + l_header_message.append_character ((n |>> 8).to_character_8) + l_header_message.append_character ( n.to_character_8) + elseif l_message_count > 125 then + l_header_message.append_code ((0x80 | 126).to_natural_32) + l_header_message.append_code ((n |>> 8).as_natural_32) + l_header_message.append_character (n.to_character_8) + else + l_header_message.append_code ((0x80 | n).as_natural_32) + end + + l_key := new_key + l_header_message.append (l_key.substring (1, 4)) + + socket.put_string (l_header_message) + + l_message := implementation.unmmask (a_message, l_key.substring (1, 4)) + + + l_chunk_size := 16_384 -- 16K + if l_message_count < l_chunk_size then + socket.put_string (l_message) + else + from + i := 0 + until + l_chunk_size = 0 + loop + debug ("ws") + print ("Sending chunk " + (i + 1).out + " -> " + (i + l_chunk_size).out +" / " + l_message_count.out + "%N") + end + l_chunk := l_message.substring (i + 1, l_message_count.min (i + l_chunk_size)) + socket.put_string (l_chunk) + if l_chunk.count < l_chunk_size then + l_chunk_size := 0 + end + i := i + l_chunk_size + end + debug ("ws") + print ("Sending chunk done%N") + end + end + else + -- FIXME: what should be done on rescue? + end + rescue + retried := True + io.put_string ("Internal error in " + generator + ".do_send (conn, a_opcode=" + a_opcode.out + ", a_message) !%N") + retry + end + + new_key: STRING + local + l_random: SALT_XOR_SHIFT_64_GENERATOR + do + create Result.make_empty + create l_random.make (4) + across + l_random.new_sequence as i + loop + Result.append_integer (i.item) + end + end + + implementation: WEB_SOCKET_IMPL + -- Web Socket implementation + + crlf: STRING = "%R%N" + +end diff --git a/library/network/websocket/client/src/web_socket_handshake_data.e b/library/network/websocket/client/src/web_socket_handshake_data.e new file mode 100644 index 00000000..ab5b9f65 --- /dev/null +++ b/library/network/websocket/client/src/web_socket_handshake_data.e @@ -0,0 +1,90 @@ +note + description: "Summary description for {WEB_SOCKET_HANDSHAKE_DATA}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET_HANDSHAKE_DATA + + +create + make + +feature {NONE} -- Initialization + + make + do + reset + end + + + reset + do + has_error := False + version := Void + create method.make_empty + create uri.make_empty + create request_header.make_empty + create request_header_map.make (10) + end + + +feature -- Access + + + request_header: STRING + -- Header' source + + request_header_map: HASH_TABLE [STRING, STRING] + -- Contains key:value of the header + + has_error: BOOLEAN + -- Error occurred during `analyze_request_message' + + method: STRING + -- http verb + + uri: STRING + -- http endpoint + + version: detachable STRING + -- http_version + +feature -- Change Element + + set_method (a_method: STRING) + -- Set `method' with `a_method' + do + method := a_method + ensure + method_set: method = a_method + end + + + set_version (a_version: STRING) + -- Set `version' with `a_version' + do + version := a_version + ensure + version_set: attached version as l_version implies l_version = a_version + end + + mark_error + do + has_error := True + end + + set_request_header (a_header: STRING) + -- Set `request_header' with `a_header' + do + request_header := a_header + ensure + request_header_set: request_header = a_header + end + + put_header (a_key: STRING; a_value : STRING) + do + request_header_map.put (a_value, a_key) + end +end diff --git a/library/network/websocket/client/src/web_socket_impl.e b/library/network/websocket/client/src/web_socket_impl.e new file mode 100644 index 00000000..3fc61c04 --- /dev/null +++ b/library/network/websocket/client/src/web_socket_impl.e @@ -0,0 +1,739 @@ +note + description: "Summary description for {WEB_SOCKET_IMPL}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET_IMPL + +inherit + + WEB_SOCKET + +create + make, make_with_port, make_with_protocols, make_with_protocols_and_port + +feature {NONE} -- Initialization + + make (a_subscriber: WEB_SOCKET_SUBSCRIBER; a_uri: READABLE_STRING_GENERAL) + -- Create a websocket instante with a default port. + do + reset + subscriber := a_subscriber + uri := a_uri + create protocol.make_empty + set_default_port + create ready_state.make + ensure + uri_set: a_uri = uri + port_wss: is_tunneled implies port = wss_port_default + port_ws: not is_tunneled implies port = ws_port_default + ready_state_set: ready_state.state = {WEB_SOCKET_READY_STATE}.connecting + subscriber_set: subscriber = a_subscriber + protocol_set: protocol.is_empty + end + + make_with_port (a_subscriber: WEB_SOCKET_SUBSCRIBER; a_uri: READABLE_STRING_GENERAL; a_port: INTEGER) + -- Create a websocket instance with port `a_port', + do + make (a_subscriber, a_uri) + port := a_port + ensure + uri_set: a_uri = uri + port_set: port = a_port + ready_state_set: ready_state.state = {WEB_SOCKET_READY_STATE}.connecting + subscriber_set: subscriber = a_subscriber + end + + make_with_protocols (a_subscriber: WEB_SOCKET_SUBSCRIBER; a_uri: READABLE_STRING_GENERAL; a_protocols: detachable LIST [STRING]) + -- Create a web socket instance with a list of protocols `a_protocols' and default port. + do + reset + subscriber := a_subscriber + uri := a_uri + create protocol.make_empty + protocols := a_protocols + set_default_port + create ready_state.make + ensure + uri_set: a_uri = uri + port_wss: is_tunneled implies port = wss_port_default + port_ws: not is_tunneled implies port = ws_port_default + protocols_set: protocols = a_protocols + ready_state_set: ready_state.state = {WEB_SOCKET_READY_STATE}.connecting + subscriber_set: subscriber = a_subscriber + protocol_set: protocol.is_empty + end + + make_with_protocols_and_port (a_subscriber: WEB_SOCKET_SUBSCRIBER; a_uri: READABLE_STRING_GENERAL; a_protocols: detachable LIST [STRING]; a_port: INTEGER) + -- Create a web socket instance with a list of protocols `a_protocols' and port `a_port'. + do + make_with_protocols (a_subscriber, a_uri, a_protocols) + port := a_port + ensure + uri_set: a_uri = uri + protocols_set: protocols = a_protocols + port_set: port = a_port + ready_state_set: ready_state.state = {WEB_SOCKET_READY_STATE}.connecting + subscriber_set: subscriber = a_subscriber + end + + reset + do + has_error := False + end + +feature -- Access + + has_error: BOOLEAN + + is_verbose: BOOLEAN + +feature -- Handshake + + start_handshake (a_handshake: STRING) + do + subscriber.on_websocket_handshake (a_handshake) + end + +feature -- Receive + + receive + local + l_frame: detachable WEB_SOCKET_FRAME + l_client_message: detachable READABLE_STRING_8 + do + l_frame := next_frame (subscriber.connection) + if l_frame /= Void and then l_frame.is_valid then + if attached l_frame.injected_control_frames as l_injections then + -- Process injected control frames now. + -- FIXME + across + l_injections as ic + loop + if ic.item.is_connection_close then + -- FIXME: we should probably send this event .. after the `l_frame.parent' frame event. + subscriber.on_websocket_close ("Normal Close") + elseif ic.item.is_ping then + -- FIXME reply only to the most recent ping ... + subscriber.on_websocket_ping (ic.item.payload_data) + elseif ic.item.is_pong then + subscriber.on_websocket_pong (ic.item.payload_data) + else + subscriber.on_websocket_error ("Wrong Opcode") + end + end + end + l_client_message := l_frame.payload_data + if l_client_message = Void then + l_client_message := "" + end + debug ("ws") + print ("%NExecute: %N") + print (" [opcode: " + opcode_name (l_frame.opcode) + "]%N") + if l_frame.is_text then + print (" [client message: %"" + l_client_message + "%"]%N") + elseif l_frame.is_binary then + print (" [client binary message length: %"" + l_client_message.count.out + "%"]%N") + end + print (" [is_control: " + l_frame.is_control.out + "]%N") + print (" [is_binary: " + l_frame.is_binary.out + "]%N") + print (" [is_text: " + l_frame.is_text.out + "]%N") + end + if l_frame.is_connection_close then + subscriber.on_websocket_close ("Normal Close") + elseif l_frame.is_binary then + subscriber.on_websocket_binary_message (l_client_message) + elseif l_frame.is_text then + subscriber.on_websocket_text_message (l_client_message) + elseif l_frame.is_ping then + subscriber.on_websocket_ping (l_client_message) + elseif l_frame.is_pong then + subscriber.on_websocket_pong (l_client_message) + else + subscriber.on_websocket_error ("Wrong Opcode") + end + else + debug ("ws") + print ("%NExecute: %N") + print (" [ERROR: invalid frame]%N") + if l_frame /= Void and then attached l_frame.error as err then + print (" [Code: " + err.code.out + "]%N") + print (" [Description: " + err.description + "]%N") + end + end + subscriber.on_websocket_close ("") + end + + -- if l_utf.is_valid_utf_8_string_8 (l_message) then + -- if is_data_frame_ok then + -- if opcode = text_frame then + -- subscriber.on_websocket_text_message (l_message) + -- elseif opcode = binary_frame then + -- subscriber.on_websocket_binary_message (l_message) + -- elseif opcode = ping_frame then + -- subscriber.on_websocket_ping (l_message) + -- elseif opcode = pong_frame then + -- subscriber.on_websocket_pong (l_message) + -- elseif opcode = Connection_close_frame then + -- subscriber.on_websocket_close ("Normal Close") + -- elseif opcode = ping_frame then + -- subscriber.on_websocket_ping (l_message) + -- elseif opcode = pong_frame then + -- subscriber.on_websocket_pong (l_message) + -- else + -- subscriber.on_websocket_error ("Wrong Opcode") + -- end + -- else + -- subscriber.on_websocket_close ("Invalid data frame") + -- end + -- else + -- subscriber.on_websocket_error ("Invalid UTF-8") + -- end + + end + +feature -- Methods + + send (a_message: STRING) + do + end + + close (a_id: INTEGER) + -- Close a websocket connection with a close id : `a_id' + do + end + + close_with_description (a_id: INTEGER; a_description: READABLE_STRING_GENERAL) + -- Close a websocket connection with a close id : `a_id' and a description `a_description' + do + end + +feature {NONE} -- Implementation + + set_default_port + do + if is_tunneled then + port := wss_port_default + else + port := ws_port_default + end + end + + subscriber: WEB_SOCKET_SUBSCRIBER + + next_frame (a_socket: HTTP_STREAM_SOCKET): detachable WEB_SOCKET_FRAME + -- TODO Binary messages + -- Handle error responses in a better way. + -- IDEA: + -- class FRAME + -- is_fin: BOOLEAN + -- opcode: WEB_SOCKET_STATUS_CODE (TEXT, BINARY, CLOSE, CONTINUE,PING, PONG) + -- data/payload + -- status_code: #see Status Codes http://tools.ietf.org/html/rfc6455#section-7.3 + -- has_error + -- + -- See Base Framing Protocol: http://tools.ietf.org/html/rfc6455#section-5.2 + -- 0 1 2 3 + -- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + -- +-+-+-+-+-------+-+-------------+-------------------------------+ + -- |F|R|R|R| opcode|M| Payload len | Extended payload length | + -- |I|S|S|S| (4) |A| (7) | (16/64) | + -- |N|V|V|V| |S| | (if payload len==126/127) | + -- | |1|2|3| |K| | | + -- +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + -- | Extended payload length continued, if payload len == 127 | + -- + - - - - - - - - - - - - - - - +-------------------------------+ + -- | |Masking-key, if MASK set to 1 | + -- +-------------------------------+-------------------------------+ + -- | Masking-key (continued) | Payload Data | + -- +-------------------------------- - - - - - - - - - - - - - - - + + -- : Payload Data continued ... : + -- + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + -- | Payload Data continued ... | + -- +---------------------------------------------------------------+ + note + EIS: "name=WebSocket RFC", "protocol=URI", "src=http://tools.ietf.org/html/rfc6455#section-5.2" + require + a_socket_in_blocking_mode: a_socket.is_blocking + local + l_opcode: INTEGER + l_len: INTEGER + l_remaining_len: INTEGER + l_payload_len: NATURAL_64 + l_chunk: STRING + l_rsv: BOOLEAN + l_fin: BOOLEAN + l_has_mask: BOOLEAN + l_chunk_size: INTEGER + l_byte: INTEGER + l_fetch_count: INTEGER + l_bytes_read: INTEGER + s: STRING + is_data_frame_ok: BOOLEAN -- Is the last process data framing ok? + retried: BOOLEAN + do + if not retried then + debug ("ws") + print ("next_frame:%N") + end + from + is_data_frame_ok := True + until + l_fin or not is_data_frame_ok + loop + -- multi-frames or continue is only valid for Binary or Text + s := next_bytes (a_socket, 1) + if s.is_empty then + is_data_frame_ok := False + debug ("ws") + print ("[ERROR] incomplete_data!%N") + end + else + l_byte := s [1].code + debug ("ws") + print (" fin,rsv(3),opcode(4)=") + print (to_byte_representation (l_byte)) + print ("%N") + end + l_fin := l_byte & (0b10000000) /= 0 + l_rsv := l_byte & (0b01110000) = 0 + l_opcode := l_byte & 0b00001111 + if Result /= Void then + if l_opcode = Result.opcode then + -- should not occur in multi-fragment frame! + create Result.make (l_opcode, l_fin) + Result.report_error (protocol_error, "Unexpected injected frame") + elseif l_opcode = continuation_frame then + -- Expected + Result.update_fin (l_fin) + elseif is_control_frame (l_opcode) then + -- Control frames (see Section 5.5) MAY be injected in the middle of + -- a fragmented message. Control frames themselves MUST NOT be fragmented. + -- if the l_opcode is a control frame then there is an error!!! + -- CLOSE, PING, PONG + create Result.make_as_injected_control (l_opcode, Result) + else + -- should not occur in multi-fragment frame! + create Result.make (l_opcode, l_fin) + Result.report_error (protocol_error, "Unexpected frame") + end + else + create Result.make (l_opcode, l_fin) + if Result.is_continuation then + -- Continuation frame is not expected without parent frame! + Result.report_error (protocol_error, "There is no message to continue!") + end + end + if Result.is_valid then + --| valid frame/fragment + if is_verbose then + log ("+ frame " + opcode_name (l_opcode) + " (fin=" + l_fin.out + ")") + end + + -- rsv validation + if not l_rsv then + -- RSV1, RSV2, RSV3: 1 bit each + + -- MUST be 0 unless an extension is negotiated that defines meanings + -- for non-zero values. If a nonzero value is received and none of + -- the negotiated extensions defines the meaning of such a nonzero + -- value, the receiving endpoint MUST _Fail the WebSocket + -- Connection_ + + -- FIXME: add support for extension ? + Result.report_error (protocol_error, "RSV values MUST be 0 unless an extension is negotiated that defines meanings for non-zero values") + end + else + if is_verbose then + log ("+ INVALID frame " + opcode_name (l_opcode) + " (fin=" + l_fin.out + ")") + end + end + + -- At the moment only TEXT, (pending Binary) + if Result.is_valid then + if Result.is_text or Result.is_binary or Result.is_control then + -- Reading next byte (mask+payload_len) + s := next_bytes (a_socket, 1) + if s.is_empty then + Result.report_error (invalid_data, "Incomplete data for mask and payload len") + else + l_byte := s [1].code + debug ("ws") + print (" mask,payload_len(7)=") + print (to_byte_representation (l_byte)) + io.put_new_line + end + l_has_mask := l_byte & (0b10000000) /= 0 -- MASK + l_len := l_byte & 0b01111111 -- 7bits + + debug ("ws") + print (" payload_len=" + l_len.out) + io.put_new_line + end + if Result.is_control and then l_len > 125 then + -- All control frames MUST have a payload length of 125 bytes or less + -- and MUST NOT be fragmented. + Result.report_error (protocol_error, "Control frame MUST have a payload length of 125 bytes or less") + elseif l_len = 127 then -- TODO proof of concept read 8 bytes. + -- the following 8 bytes interpreted as a 64-bit unsigned integer + -- (the most significant bit MUST be 0) are the payload length. + -- Multibyte length quantities are expressed in network byte order. + s := next_bytes (a_socket, 8) -- 64 bits + debug ("ws") + print (" extended payload length=" + string_to_byte_representation (s)) + io.put_new_line + end + if s.count < 8 then + Result.report_error (Invalid_data, "Incomplete data for 64 bit Extended payload length") + else + l_payload_len := s [8].natural_32_code.to_natural_64 + l_payload_len := l_payload_len | (s [7].natural_32_code.to_natural_64 |<< 8) + l_payload_len := l_payload_len | (s [6].natural_32_code.to_natural_64 |<< 16) + l_payload_len := l_payload_len | (s [5].natural_32_code.to_natural_64 |<< 24) + l_payload_len := l_payload_len | (s [4].natural_32_code.to_natural_64 |<< 32) + l_payload_len := l_payload_len | (s [3].natural_32_code.to_natural_64 |<< 40) + l_payload_len := l_payload_len | (s [2].natural_32_code.to_natural_64 |<< 48) + l_payload_len := l_payload_len | (s [1].natural_32_code.to_natural_64 |<< 56) + end + elseif l_len = 126 then + s := next_bytes (a_socket, 2) -- 16 bits + debug ("ws") + print (" extended payload length bits=" + string_to_byte_representation (s)) + io.put_new_line + end + if s.count < 2 then + Result.report_error (Invalid_data, "Incomplete data for 16 bit Extended payload length") + else + l_payload_len := s [2].natural_32_code.to_natural_64 + l_payload_len := l_payload_len | (s [1].natural_32_code.to_natural_64 |<< 8) + end + else + l_payload_len := l_len.to_natural_64 + end + debug ("ws") + print (" Full payload length=" + l_payload_len.out) + io.put_new_line + end + if Result.is_valid then + if l_has_mask then + Result.report_error (protocol_error, "Server sent a masked frame!") + else + end + if Result.is_valid then + l_chunk_size := 0x4000 -- 16 K + if l_payload_len > {INTEGER_32}.max_value.to_natural_64 then + -- Issue .. to big to store in STRING + -- FIXME !!! + Result.report_error (Message_too_large, "Can not handle payload data (len=" + l_payload_len.out + ")") + else + l_len := l_payload_len.to_integer_32 + end + from + l_fetch_count := 0 + l_remaining_len := l_len + until + l_fetch_count >= l_len or l_len = 0 or not Result.is_valid + loop + if l_remaining_len < l_chunk_size then + l_chunk_size := l_remaining_len + end + a_socket.read_stream (l_chunk_size) + l_bytes_read := a_socket.bytes_read + debug ("ws") + print ("read chunk size=" + l_chunk_size.out + " fetch_count=" + l_fetch_count.out + " l_len=" + l_len.out + " -> " + l_bytes_read.out + "bytes%N") + end + if a_socket.bytes_read > 0 then + l_remaining_len := l_remaining_len - l_bytes_read + l_chunk := a_socket.last_string + l_fetch_count := l_fetch_count + l_bytes_read + Result.append_payload_data_chop (l_chunk, l_bytes_read, l_remaining_len = 0) + else + Result.report_error (internal_error, "Issue reading payload data...") + end + end + if is_verbose then + log (" Received " + l_fetch_count.out + " out of " + l_len.out + " bytes <===============") + end + debug ("ws") + print (" -> ") + if attached Result.payload_data as l_payload_data then + s := l_payload_data.tail (l_fetch_count) + if s.count > 50 then + print (string_to_byte_hexa_representation (s.head (50) + "..")) + else + print (string_to_byte_hexa_representation (s)) + end + print ("%N") + if Result.is_text and Result.is_fin and Result.fragment_count = 0 then + print (" -> ") + if s.count > 50 then + print (s.head (50) + "..") + else + print (s) + end + print ("%N") + end + end + end + end + end + end + end + end + end + if Result /= Void then + if attached Result.error as err then + if is_verbose then + log (" !Invalid frame: " + err.string) + end + end + if Result.is_injected_control then + if attached Result.parent as l_parent then + if not Result.is_valid then + l_parent.report_error (protocol_error, "Invalid injected frame") + end + if Result.is_connection_close then + -- Return this and process the connection close right away! + l_parent.update_fin (True) + l_fin := Result.is_fin + else + Result := l_parent + l_fin := l_parent.is_fin + check + -- This is a control frame but occurs in fragmented frame. + inside_fragmented_frame: not l_fin + end + end + else + check + has_parent: False + end + l_fin := False -- This is a control frame but occurs in fragmented frame. + end + end + if not Result.is_valid then + is_data_frame_ok := False + end + else + is_data_frame_ok := False + end + end + end + rescue + retried := True + if Result /= Void then + Result.report_error (internal_error, "Internal error") + end + retry + end + + next_bytes (a_socket: HTTP_STREAM_SOCKET; nb: INTEGER): STRING + require + nb > 0 + local + n, l_bytes_read: INTEGER + do + create Result.make (nb) + from + n := nb + until + n = 0 + loop + a_socket.read_stream (nb) + l_bytes_read := a_socket.bytes_read + if l_bytes_read > 0 then + Result.append (a_socket.last_string) + n := n - l_bytes_read + else + n := 0 + end + end + end + + unmask (a_chunk: STRING_8; a_pos: INTEGER; a_key: READABLE_STRING_8) + local + i, n: INTEGER + do + from + i := 1 + n := a_chunk.count + until + i > n + loop + a_chunk.put_code (a_chunk.code (i).bit_xor (a_key [((i + (a_pos - 1) - 1) \\ 4) + 1].natural_32_code), i) + i := i + 1 + end + end + + append_chunk_unmasked (a_chunk: READABLE_STRING_8; a_pos: INTEGER; a_key: READABLE_STRING_8; a_target: STRING) + -- To convert masked data into unmasked data, or vice versa, the following + -- algorithm is applied. The same algorithm applies regardless of the + -- direction of the translation, e.g., the same steps are applied to + -- mask the data as to unmask the data. + + -- Octet i of the transformed data ("transformed-octet-i") is the XOR of + -- octet i of the original data ("original-octet-i") with octet at index + -- i modulo 4 of the masking key ("masking-key-octet-j"): + + -- j = i MOD 4 + -- transformed-octet-i = original-octet-i XOR masking-key-octet-j + + -- The payload length, indicated in the framing as frame-payload-length, + -- does NOT include the length of the masking key. It is the length of + -- the "Payload data", e.g., the number of bytes following the masking + -- key. + note + EIS: "name=Masking", "src=http://tools.ietf.org/html/rfc6455#section-5.3", "protocol=uri" + local + i, n: INTEGER + do + -- debug ("ws") + -- print ("append_chunk_unmasked (%"" + string_to_byte_representation (a_chunk) + "%",%N%Ta_pos=" + a_pos.out+ ", a_key, a_target #.count=" + a_target.count.out + ")%N") + -- end + from + i := 1 + n := a_chunk.count + until + i > n + loop + a_target.append_code (a_chunk.code (i).bit_xor (a_key [((i + (a_pos - 1) - 1) \\ 4) + 1].natural_32_code)) + i := i + 1 + end + end + +feature {NONE} -- Debug + + log (m: STRING) + do + io.put_string (m + "%N") + end + + to_byte (a_integer: INTEGER): ARRAY [INTEGER] + require + valid: a_integer >= 0 and then a_integer <= 255 + local + l_val: INTEGER + l_index: INTEGER + do + create Result.make_filled (0, 1, 8) + from + l_val := a_integer + l_index := 8 + until + l_val < 2 + loop + Result.put (l_val \\ 2, l_index) + l_val := l_val // 2 + l_index := l_index - 1 + end + Result.put (l_val, l_index) + end + +feature -- Masking + + unmmask (a_frame: READABLE_STRING_8; a_key: READABLE_STRING_8): STRING + -- To convert masked data into unmasked data, or vice versa, the following + -- algorithm is applied. The same algorithm applies regardless of the + -- direction of the translation, e.g., the same steps are applied to + -- mask the data as to unmask the data. + + -- Octet i of the transformed data ("transformed-octet-i") is the XOR of + -- octet i of the original data ("original-octet-i") with octet at index + -- i modulo 4 of the masking key ("masking-key-octet-j"): + + -- j = i MOD 4 + -- transformed-octet-i = original-octet-i XOR masking-key-octet-j + + -- The payload length, indicated in the framing as frame-payload-length, + -- does NOT include the length of the masking key. It is the length of + -- the "Payload data", e.g., the number of bytes following the masking + -- key. + note + EIS: "name=Masking", "src=S", "protocol=uri" + local + l_frame: STRING + i: INTEGER + do + l_frame := a_frame.twin + from + i := 1 + until + i > l_frame.count + loop + l_frame [i] := (l_frame [i].code.to_integer_8.bit_xor (a_key [((i - 1) \\ 4) + 1].code.to_integer_8)).to_character_8 + i := i + 1 + end + Result := l_frame + end + + to_byte_representation (a_integer: INTEGER): STRING + require + valid: a_integer >= 0 and then a_integer <= 255 + local + l_val: INTEGER + do + create Result.make (8) + from + l_val := a_integer + until + l_val < 2 + loop + Result.prepend_integer (l_val \\ 2) + l_val := l_val // 2 + end + Result.prepend_integer (l_val) + end + + string_to_byte_representation (s: STRING): STRING + require + valid: s.count > 0 + local + i, n: INTEGER + do + n := s.count + create Result.make (8 * n) + if n > 0 then + from + i := 1 + until + i > n + loop + if not Result.is_empty then + Result.append_character (':') + end + Result.append (to_byte_representation (s [i].code)) + i := i + 1 + end + end + end + + string_to_byte_hexa_representation (s: STRING): STRING + local + i, n: INTEGER + c: INTEGER + do + n := s.count + create Result.make (8 * n) + if n > 0 then + from + i := 1 + until + i > n + loop + if not Result.is_empty then + Result.append_character (':') + end + c := s [i].code + check + c <= 0xFF + end + Result.append_character (((c |>> 4) & 0xF).to_hex_character) + Result.append_character (((c) & 0xF).to_hex_character) + i := i + 1 + end + end + end + +end diff --git a/library/network/websocket/client/src/web_socket_null_client.e b/library/network/websocket/client/src/web_socket_null_client.e new file mode 100644 index 00000000..38879949 --- /dev/null +++ b/library/network/websocket/client/src/web_socket_null_client.e @@ -0,0 +1,30 @@ +note + description: "[ + Objects that is used to create dummy web socket client object + used for void-safety + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET_NULL_CLIENT + +inherit + WEB_SOCKET_SUBSCRIBER + +feature -- Handshake + + on_websocket_handshake (request: STRING) + -- Default do nothing + do + end + +feature -- TCP connection + + connection: HTTP_STREAM_SOCKET + do + create Result.make_client_by_port (0, "null") + end + +end diff --git a/library/network/websocket/client/src/web_socket_ready_state.e b/library/network/websocket/client/src/web_socket_ready_state.e new file mode 100644 index 00000000..5adfac5e --- /dev/null +++ b/library/network/websocket/client/src/web_socket_ready_state.e @@ -0,0 +1,120 @@ +note + description: "Report the state of the connection: [CONNECTING, OPEN, CLOSING, CLOSED]" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET_READY_STATE + +create + make + +feature -- Initialization + + make + do + set_state (Connecting) + ensure + state_connecting: state = Connecting + end +feature -- Access + + state: INTEGER + -- Current ready state + +feature -- Status Report + + is_valid_state (a_state: INTEGER): BOOLEAN + -- Is `a_state' a valid ready state? + do + Result := Connecting = a_state or else Open = a_state or else Closing = a_state or else Closed = a_state + end + + is_connecting: BOOLEAN + -- Is current `state' connecting? + do + Result := state = Connecting + end + + is_open: BOOLEAN + -- Is current `state' open? + do + Result := state = Open + end + + is_closing: BOOLEAN + -- Is current `state' closing? + do + Result := state = Closing + end + + is_closed: BOOLEAN + -- Is current `state' closed? + do + Result := state = Closed + end + +feature -- Change Element + + set_state (a_state: INTEGER) + -- Set `state' to `a_state' + require + valid_state: is_valid_state (a_state) + do + state := a_state + ensure + state_set: state = a_state + end + + mark_connecting + --Set `state' to `connecting' + do + set_state (Connecting) + ensure + connecting_state: state = Connecting + end + + mark_open + --Set `state' to `open' + do + set_state (Open) + ensure + open_state: state = Open + end + + mark_closing + --Set `state' to `closing' + do + set_state (Closing) + ensure + open_state: state = Closing + end + + + mark_closed + --Set `state' to `closed' + do + set_state (Closed) + ensure + open_state: state = Closed + end + + +feature --State connection + + Connecting: INTEGER = 0 + -- The connection is in progress but has not been established. + + Open: INTEGER = 1 + -- The connection has been stablished. Messages can flow between client and server. + + Closing: INTEGER = 2 + -- The connection is going through the closing handshake + + Closed: INTEGER = 3 + -- The connection has been closed or could not be opened. + + +invariant + is_valid_sate: is_valid_state (state) +end diff --git a/library/network/websocket/client/src/web_socket_subscriber.e b/library/network/websocket/client/src/web_socket_subscriber.e new file mode 100644 index 00000000..35484330 --- /dev/null +++ b/library/network/websocket/client/src/web_socket_subscriber.e @@ -0,0 +1,61 @@ +note + description: "Summary description for {WEB_SOCKET_SUBSCRIBER}." + author: "" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEB_SOCKET_SUBSCRIBER + + +feature -- Events + + on_websocket_ping (a_message: detachable STRING) + -- Send a ping in response to a received ping. + do + end + + + on_websocket_pong (a_message: detachable STRING) + -- Default implementation do nothing. + do + end + + + on_websocket_text_message (a_message: detachable STRING) + -- Default implementation do nothing. + do + end + + on_websocket_binary_message (a_message: detachable STRING) + -- Default implementation do nothing. + do + end + + + on_websocket_open (a_message: detachable STRING) + do + end + + + on_websocket_close (a_message: detachable STRING) + do + end + + on_websocket_error (a_error: detachable STRING) + do + end + +feature -- Handshake + + on_websocket_handshake (request: STRING) + -- Default do nothing + deferred + end + +feature -- TCP connection + + connection: HTTP_STREAM_SOCKET + deferred + end +end diff --git a/library/network/websocket/client/web_socket_client-safe.ecf b/library/network/websocket/client/web_socket_client-safe.ecf new file mode 100644 index 00000000..33bf7579 --- /dev/null +++ b/library/network/websocket/client/web_socket_client-safe.ecf @@ -0,0 +1,45 @@ + + + + + + /.git$ + /EIFGENs$ + /CVS$ + /.svn$ + + + + + + + + + + + + + + /no_ssl$ + /ssl$ + /spec$ + + + + + + + + + + + + + + + + + + diff --git a/library/network/websocket/client/web_socket_client.ecf b/library/network/websocket/client/web_socket_client.ecf new file mode 100644 index 00000000..78bdc0d0 --- /dev/null +++ b/library/network/websocket/client/web_socket_client.ecf @@ -0,0 +1,64 @@ + + + + + + /.git$ + /EIFGENs$ + /CVS$ + /.svn$ + + + + + + + + + + + + + + + + + + /socket$ + /no_ssl$ + /ssl$ + /spec$ + + + + + + + + + + + + + + /tcp_stream_socket.e$ + + + + + + + + + + + + + + diff --git a/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_constants.e b/library/network/websocket/protocol/web_socket_constants.e similarity index 100% rename from library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_constants.e rename to library/network/websocket/protocol/web_socket_constants.e diff --git a/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_error_frame.e b/library/network/websocket/protocol/web_socket_error_frame.e similarity index 100% rename from library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_error_frame.e rename to library/network/websocket/protocol/web_socket_error_frame.e diff --git a/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_frame.e b/library/network/websocket/protocol/web_socket_frame.e similarity index 100% rename from library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_frame.e rename to library/network/websocket/protocol/web_socket_frame.e diff --git a/library/network/websocket/protocol/web_socket_protocol-safe.ecf b/library/network/websocket/protocol/web_socket_protocol-safe.ecf new file mode 100644 index 00000000..82a6cec8 --- /dev/null +++ b/library/network/websocket/protocol/web_socket_protocol-safe.ecf @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/library/network/websocket/protocol/web_socket_protocol.ecf b/library/network/websocket/protocol/web_socket_protocol.ecf new file mode 100644 index 00000000..2fd61c67 --- /dev/null +++ b/library/network/websocket/protocol/web_socket_protocol.ecf @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/library/server/ewsgi/connectors/standalone/standalone-safe.ecf b/library/server/ewsgi/connectors/standalone/standalone-safe.ecf index ef21ab31..c9faf126 100644 --- a/library/server/ewsgi/connectors/standalone/standalone-safe.ecf +++ b/library/server/ewsgi/connectors/standalone/standalone-safe.ecf @@ -16,7 +16,6 @@ -