From b49e841ac7e0f8c14388822e3c5ef20a2ff451d5 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Tue, 21 Jun 2016 23:37:48 +0200 Subject: [PATCH] Added WSF `standalone_websocket` connector, that provides websocket on top of `standalone` connector. --- .../standalone/standalone_websocket-safe.ecf | 24 + .../connector/standalone_websocket-safe.ecf | 25 + .../websocket/event/web_socket_event_i.e | 183 +++++ .../websocket/event/web_socket_message_type.e | 39 + .../websocket/protocol/web_socket_constants.e | 203 +++++ .../protocol/web_socket_error_frame.e | 35 + .../websocket/protocol/web_socket_frame.e | 437 ++++++++++ .../websocket/web_socket.e | 754 ++++++++++++++++++ .../wgi_standalone_websocket_connector.e | 44 + ...sf_standalone_websocket_service_launcher.e | 49 ++ .../wsf_websocket_execution.e | 240 ++++++ 11 files changed, 2033 insertions(+) create mode 100644 library/server/ewsgi/connectors/standalone/standalone_websocket-safe.ecf create mode 100644 library/server/wsf/connector/standalone_websocket-safe.ecf create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_message_type.e create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_constants.e create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_error_frame.e create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_frame.e create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/web_socket.e create mode 100644 library/server/wsf/connector/standalone_websocket/wgi_standalone_websocket_connector.e create mode 100644 library/server/wsf/connector/standalone_websocket/wsf_standalone_websocket_service_launcher.e create mode 100644 library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e diff --git a/library/server/ewsgi/connectors/standalone/standalone_websocket-safe.ecf b/library/server/ewsgi/connectors/standalone/standalone_websocket-safe.ecf new file mode 100644 index 00000000..1955bb7a --- /dev/null +++ b/library/server/ewsgi/connectors/standalone/standalone_websocket-safe.ecf @@ -0,0 +1,24 @@ + + + + + + /EIFGENs$ + /\.git$ + /\.svn$ + + + + + + + + + + + + diff --git a/library/server/wsf/connector/standalone_websocket-safe.ecf b/library/server/wsf/connector/standalone_websocket-safe.ecf new file mode 100644 index 00000000..01484e5b --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket-safe.ecf @@ -0,0 +1,25 @@ + + + + + + /EIFGENs$ + /\.git$ + /\.svn$ + + + + + + + + + + + + + + + + diff --git a/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e b/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e new file mode 100644 index 00000000..b8e8994d --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e @@ -0,0 +1,183 @@ +note + description: "[ + API to perform actions like opening and closing the connection, sending and receiving messages, and listening + for events. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEB_SOCKET_EVENT_I + +inherit + WEB_SOCKET_CONSTANTS + + REFACTORING_HELPER + +feature -- Web Socket Interface + + on_event (conn: HTTPD_STREAM_SOCKET; a_message: detachable READABLE_STRING_8; a_opcode: INTEGER) + -- Called when a frame from the client has been receive + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + local + l_message: READABLE_STRING_8 + do + debug ("ws") + print ("%Non_event (conn, a_message, " + opcode_name (a_opcode) + ")%N") + end + if a_message = Void then + create {STRING} l_message.make_empty + else + l_message := a_message + end + + if a_opcode = Binary_frame then + on_binary (conn, l_message) + elseif a_opcode = Text_frame then + on_text (conn, l_message) + elseif a_opcode = Pong_frame then + on_pong (conn, l_message) + elseif a_opcode = Ping_frame then + on_ping (conn, l_message) + elseif a_opcode = Connection_close_frame then + on_connection_close (conn, "") + else + on_unsupported (conn, l_message, a_opcode) + end + end + + on_open (conn: HTTPD_STREAM_SOCKET) + -- Called after handshake, indicates that a complete WebSocket connection has been established. + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + deferred + end + + on_binary (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + deferred + end + + on_pong (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + do + -- log ("Its a pong frame") + -- at first we ignore pong + -- FIXME: provide better explanation + end + + on_ping (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + do + send (conn, Pong_frame, a_message) + end + + on_text (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + deferred + end + + on_unsupported (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8; a_opcode: INTEGER) + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + do + -- do nothing + end + + on_connection_close (conn: HTTPD_STREAM_SOCKET; a_message: detachable READABLE_STRING_8) + require + conn_attached: conn /= Void + conn_valid: conn.is_open_read and then conn.is_open_write + do + send (conn, Connection_close_frame, "") + end + + on_close (conn: detachable HTTPD_STREAM_SOCKET) + -- Called after the WebSocket connection is closed. + deferred + end + +feature {NONE} -- Implementation + + send (conn: HTTPD_STREAM_SOCKET; 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 + do + print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N") + 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 ((0 | 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 ((0 | 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 (n.as_natural_32) + end + conn.put_string (l_header_message) + + + l_chunk_size := 16_384 -- 16K + if l_message_count < l_chunk_size then + conn.put_string (a_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 := a_message.substring (i + 1, l_message_count.min (i + l_chunk_size)) + conn.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 + +end diff --git a/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_message_type.e b/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_message_type.e new file mode 100644 index 00000000..cd48af00 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_message_type.e @@ -0,0 +1,39 @@ +note + description: "[ + A web socket message has an opcode specifying the type of the message payload. The + opcode consists of the last four bits in the first byte of the frame header. + ]" + date: "$Date$" + revision: "$Revision$" + EIS: "name=Data Frame", "src=http://tools.ietf.org/html/rfc6455#section-5.6", "protocol=uri" + EIS: "name=Control Frame", "src=http://tools.ietf.org/html/rfc6455#section-5.5", "protocol=uri" + +class + WEB_SOCKET_MESSAGE_TYPE + +feature -- Data Frames + + Text: INTEGER = 0x1 + -- The data type of the message is text. + + Binary: INTEGER = 0x2 + -- The data type of the message is binary. + +feature -- Control Frames + + Close: INTEGER = 0x8 + -- The client or server is sending a closing + -- handshake to the server or client. + + Ping: INTEGER = 0x9 + -- The client or server sends a ping to the server or client. + + Pong: INTEGER = 0xA + -- The client or server sends a pong to the server or client. + +feature -- Reserverd + + -- Opcodes 0x3-0x7 are reserved for further non-control frames yet to be + -- defined. + +end diff --git a/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_constants.e b/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_constants.e new file mode 100644 index 00000000..86e771df --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_constants.e @@ -0,0 +1,203 @@ +note + description: "Constants for WebSockets" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET_CONSTANTS + +feature -- Constants + + + HTTP_1_1: STRING = "HTTP/1.1 101 WebSocket Protocol Handshake" + + Upgrade_ws: STRING = "Upgrade: websocket" + + Connection_ws: STRING = "Connection: Upgrade" + + Sec_WebSocket_Origin: STRING = "Sec-WebSocket-Origin: " + + Sec_WebSocket_Protocol: STRING = "Sec-WebSocket-Protocol: " + + Sec_WebSocket_Location: STRING = "Sec-WebSocket-Location: " + + Sec_WebSocket_Version: STRING = "Sec-WebSocket-Version: " + + Sec_WebSocket_Extensions: STRING = "Sec-WebSocket-Extensions: " + + WebSocket_Origin: STRING = "WebSocket-Origin: " + + WebSocket_Protocol: STRING = "WebSocket-Protocol: " + + WebSocket_Location: STRING = "WebSocket-Location: " + + Origin: STRING = "Origin" + + Server: STRING = "EWSS" + + Sec_WebSocket_Key: STRING = "Sec-WebSocket-Key" + + Ws_scheme: STRING = "ws://" + + Wss_scheme: STRING = "wss://" + + Magic_guid: STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + -- The handshake from the client looks as follows: + + -- GET /chat HTTP/1.1 + -- Host: server.example.com + -- Upgrade: websocket + -- Connection: Upgrade + -- Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== + -- Origin: http://example.com + -- Sec-WebSocket-Protocol: chat, superchat + -- Sec-WebSocket-Version: 13 + + -- The handshake from the server looks as follows: + + -- HTTP/1.1 101 Switching Protocols + -- Upgrade: websocket + -- Connection: Upgrade + -- Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= + -- Sec-WebSocket-Protocol: chat + +feature -- Opcodes Standard actions + + --| Maybe we need an enum STANDARD_ACTIONS_OPCODES? + -- |Opcode | Meaning | Reference | + -- -+--------+-------------------------------------+-----------| + -- | 0 | Continuation Frame | RFC 6455 | + -- -+--------+-------------------------------------+-----------| + -- | 1 | Text Frame | RFC 6455 | + -- -+--------+-------------------------------------+-----------| + -- | 2 | Binary Frame | RFC 6455 | + -- -+--------+-------------------------------------+-----------| + -- | 8 | Connection Close Frame | RFC 6455 | + -- -+--------+-------------------------------------+-----------| + -- | 9 | Ping Frame | RFC 6455 | + -- -+--------+-------------------------------------+-----------| + -- | 10 | Pong Frame | RFC 6455 | + -- -+--------+-------------------------------------+-----------| + + Continuation_frame: INTEGER = 0 + + Text_frame: INTEGER = 1 + + Binary_frame: INTEGER = 2 + + Connection_close_frame: INTEGER = 8 + + Ping_frame: INTEGER = 9 + + Pong_frame: INTEGER = 10 + + is_control_frame (a_opcode: INTEGER): BOOLEAN + -- Is `a_opcode' a control frame? + do + inspect a_opcode + when Connection_close_frame, Ping_frame, Pong_frame then + Result := True + else + end + end + + opcode_name (a_opcode: INTEGER): STRING + do + inspect a_opcode + when Continuation_frame then Result := "Continuation" + when Text_frame then Result := "Text" + when Binary_frame then Result := "Binary" + when Connection_close_frame then Result := "Connection Close" + when Ping_frame then Result := "Ping" + when Pong_frame then Result := "Pong" + else + Result := "Unknown-Opcode" + end + Result := "0x" + a_opcode.to_hex_string + " " + Result + end + +feature -- Close code numbers + + -- Maybe an ENUM CLOSE_CODES + + -- |Status Code | Meaning | Contact | Reference | + -- -+------------+-----------------+---------------+-----------| + -- | 1000 | Normal Closure | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1001 | Going Away | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1002 | Protocol error | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1003 | Unsupported Data| hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1004 | ---Reserved---- | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1005 | No Status Rcvd | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1006 | Abnormal Closure| hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1007 | Invalid frame | hybi@ietf.org | RFC 6455 | + -- | | payload data | | | + -- -+------------+-----------------+---------------+-----------| + -- | 1008 | Policy Violation| hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1009 | Message Too Big | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1010 | Mandatory Ext. | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + -- | 1011 | Internal Server | hybi@ietf.org | RFC 6455 | + -- | | Error | | | + -- -+------------+-----------------+---------------+-----------| + -- | 1015 | TLS handshake | hybi@ietf.org | RFC 6455 | + -- -+------------+-----------------+---------------+-----------| + + Normal_closure: INTEGER = 1000 + -- Indicates a normal closure, meaning that the purpose for + -- which the connection was established has been fulfilled. + + Going_away: INTEGER = 1001 + -- Indicates that an endpoint is "going away", such as a server + -- going down or a browser having navigated away from a page. + + Protocol_error: INTEGER = 1002 + -- Indicates that an endpoint is terminating the connection due + -- to a protocol error. + + Unsupported_data: INTEGER = 1003 + -- Indicates that an endpoint is terminating the connection + -- because it has received a type of data it cannot accept (e.g., an + -- endpoint that understands only text data MAY send this if it + -- receives a binary message). + + Invalid_data: INTEGER = 1007 + -- Indicates that an endpoint is terminating the connection + -- because it has received data within a message that was not + -- consistent with the type of the message (e.g., non-UTF-8 [RFC3629] + -- data within a text message). + + Policy_violation: INTEGER = 1008 + -- Indicates that an endpoint is terminating the connection + -- because it has received a message that violates its policy. This + -- is a generic status code that can be returned when there is no + -- other more suitable status code (e.g., 1003 or 1009) or if there + -- is a need to hide specific details about the policy. + + Message_too_large: INTEGER = 1009 + -- Indicates that an endpoint is terminating the connection + -- because it has received a message that is too big for it to + -- process. + + Extension_required: INTEGER = 1010 + -- Indicates that an endpoint (client) is terminating the + -- connection because it has expected the server to negotiate one or + -- more extension, but the server didn't return them in the response + -- message of the WebSocket handshake. + + Internal_error: INTEGER = 1011 + -- Indicates that a server is terminating the connection because + -- it encountered an unexpected condition that prevented it from + -- fulfilling the request. + + +end diff --git a/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_error_frame.e b/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_error_frame.e new file mode 100644 index 00000000..af9aa833 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_error_frame.e @@ -0,0 +1,35 @@ +note + description: "Summary description for {WEB_SOCKET_ERROR_FRAME}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET_ERROR_FRAME + +create + make + +feature {NONE} -- Initialization + + make (a_code: INTEGER; a_desc: like description) + do + code := a_code + description := a_desc + end + +feature -- Access + + code: INTEGER + + description: READABLE_STRING_8 + +feature -- Conversion + + string: STRING + do + create Result.make_from_string ("Error(" + code.out + "): " + description) + end + + +end diff --git a/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_frame.e b/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_frame.e new file mode 100644 index 00000000..3f3350a0 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/protocol/web_socket_frame.e @@ -0,0 +1,437 @@ +note + description: "[ + Summary description for {WEB_SOCKET_FRAME}. + 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 ... | + +---------------------------------------------------------------+ + + Check the `check_utf_8_validity_on_chop' if there is performance issue + with bigger data. + ]" + date: "$Date$" + revision: "$Revision$" + EIS: "name=Websocket RFC6455 section-5.2", "protocol=URI", "src=http://tools.ietf.org/html/rfc6455#section-5.2", "tag=rfc" + +class + WEB_SOCKET_FRAME + +inherit + ANY + + WEB_SOCKET_CONSTANTS + +create + make, + make_as_injected_control + +feature {NONE} -- Initialization + + make (a_opcode: INTEGER; flag_is_fin: BOOLEAN) + -- Create current frame with opcode `a_opcode' + -- and `a_fin' to indicate if this is the final fragment. + do + is_incomplete := False + opcode := a_opcode + is_fin := flag_is_fin + + inspect opcode + when + Continuation_frame, -- 0 + Text_frame, -- 1 + Binary_frame -- 2 + then + --| Supported opcode + when + Connection_close_frame, -- 8 + Ping_frame, -- 9 + Pong_frame -- 10 + then + --| Supported control opcode + -- All control frames MUST have a payload length of 125 bytes or less + -- and MUST NOT be fragmented. + if flag_is_fin then + -- So far it is valid. + else + report_error (Protocol_error, "Control frames MUST NOT be fragmented.") + end + else + report_error (Protocol_error, "Unknown opcode") + end + end + + make_as_injected_control (a_opcode: INTEGER; a_parent: WEB_SOCKET_FRAME) + require + parent_is_not_control_frame: not a_parent.is_control + a_opcode_is_control_frame: is_control_frame (a_opcode) + do + make (a_opcode, True) + parent := a_parent + a_parent.add_injected_control_frame (Current) + end + +feature -- Access + + opcode: INTEGER + -- CONTINUOUS, TEXT, BINARY, PING, PONG, CLOSING + + is_fin: BOOLEAN + -- is the final fragment in a message? + + fragment_count: INTEGER + + payload_length: NATURAL_64 + payload_data: detachable STRING_8 + -- Maybe we need a buffer here. + + uncoded_payload_data: detachable STRING_32 + local + utf: UTF_CONVERTER + do + if attached payload_data as d then + Result := utf.utf_8_string_8_to_string_32 (d) + end + end + + error: detachable WEB_SOCKET_ERROR_FRAME + -- Describe the type of error + +feature -- Access: injected control frames + + injected_control_frames: detachable LIST [WEB_SOCKET_FRAME] + + parent: detachable WEB_SOCKET_FRAME + -- If Current is injected, `parent' is the related fragmented frame + + is_injected_control: BOOLEAN + do + Result := parent /= Void + ensure + Result implies (is_control_frame (opcode)) + end + +feature -- Operation + + update_fin (a_flag_is_fin: BOOLEAN) + do + is_fin := a_flag_is_fin + end + +feature {WEB_SOCKET_FRAME} -- Change: injected control frames + + add_injected_control_frame (f: WEB_SOCKET_FRAME) + require + Current_is_not_control: not is_control + f_is_control_frame: f.is_control + parented_to_current: f.parent = Current + local + lst: like injected_control_frames + do + lst := injected_control_frames + if lst = Void then + create {ARRAYED_LIST [WEB_SOCKET_FRAME]} lst.make (1) + injected_control_frames := lst + end + lst.force (f) + ensure + parented_to_current: f.parent = Current + end + + remove_injected_control_frame (f: WEB_SOCKET_FRAME) + require + Current_is_not_control: not is_control + f_is_control_frame: f.is_control + parented_to_current: f.parent = Current + local + lst: like injected_control_frames + do + lst := injected_control_frames + if lst /= Void then + lst.prune (f) + if lst.is_empty then + injected_control_frames := Void + end + end + end + +feature -- Query + + is_binary: BOOLEAN + do + Result := opcode = binary_frame + end + + is_text: BOOLEAN + do + Result := opcode = text_frame + end + + is_continuation: BOOLEAN + do + Result := opcode = continuation_frame + end + + is_connection_close: BOOLEAN + do + Result := opcode = connection_close_frame + end + + is_control: BOOLEAN + do + inspect opcode + when connection_close_frame, Ping_frame, Pong_frame then + Result := True + else + end + end + + is_ping: BOOLEAN + do + Result := opcode = ping_frame + end + + is_pong: BOOLEAN + do + Result := opcode = pong_frame + end + +feature -- Status report + + is_valid: BOOLEAN + do + Result := not has_error + end + + is_incomplete: BOOLEAN + + has_error: BOOLEAN + do + Result := error /= Void + end + +feature -- Change + + increment_fragment_count + do + fragment_count := fragment_count + 1 + end + + check_utf_8_validity_on_chop: BOOLEAN = False + -- True: check for each chop + -- False: check only for each fragment + --| see autobahntestsuite #6.4.3 and #6.4.4 + + append_payload_data_chop (a_data: STRING_8; a_len: INTEGER; a_flag_chop_complete: BOOLEAN) + do + if a_flag_chop_complete then + increment_fragment_count + end + if attached payload_data as l_payload_data then + l_payload_data.append (a_data) + else + payload_data := a_data + end + payload_length := payload_length + a_len.to_natural_64 + + if is_text then + if is_fin and a_flag_chop_complete then + -- Check the whole message is a valid UTF-8 string + if attached payload_data as d then + if not is_valid_utf_8_string (d) then + report_error (invalid_data, "The text message is not a valid UTF-8 text!") + end + else + -- empty payload?? + end + elseif check_utf_8_validity_on_chop or else a_flag_chop_complete then + -- Check the payload data as utf-8 stream (may be incomplete at this point) + if not is_valid_text_payload_stream then + report_error (invalid_data, "This is not a valid UTF-8 stream!") + -- is_valid implies the connection will be closed! + end + end + end + end + + report_error (a_code: INTEGER; a_description: READABLE_STRING_8) + require + not has_error + do + create error.make (a_code, a_description) + ensure + has_error: has_error + is_not_valid: not is_valid + end + +feature {NONE} -- Helper + + last_utf_8_stream_validation_position: INTEGER + -- In relation with `is_valid_utf_8 (.., a_is_stream=True)' + + is_valid_text_payload_stream: BOOLEAN + require + is_text_frame: is_text + do + if attached payload_data as s then + Result := is_valid_utf_8 (s, not is_fin) + end + end + + is_valid_utf_8_string (s: READABLE_STRING_8): BOOLEAN + do + Result := is_valid_utf_8 (s, False) +-- and (create {UTF_CONVERTER}).is_valid_utf_8_string_8 (s) + end + + is_valid_utf_8 (s: READABLE_STRING_8; a_is_stream: BOOLEAN): BOOLEAN + -- UTF-8 validity checker. + note + EIS: "name=UTF-8 RFC3629", "protocol=URI", "src=https://tools.ietf.org/html/rfc3629", "tag=rfc" + require + is_text_frame: is_text + local + i: like {STRING_8}.count + n: like {STRING_8}.count + c,c2,c3,c4,w: NATURAL_32 + l_is_incomplete_stream: BOOLEAN + do + Result := True + -- Following code also check that codepoint is between 0 and 0x10FFFF + -- (as expected by spec, and tested by autobahn ws testsuite) + from + if a_is_stream then + i := last_utf_8_stream_validation_position -- to avoid recomputing from the beginning each time. + else + i := 0 + end + n := s.count + until + i >= n or not Result + loop + i := i + 1 + c := s.code (i) + if c <= 0x7F then + -- 0xxxxxxx + w := c + elseif c <= 0xC1 then + -- The octet values C0, C1, F5 to FF never appear. + --| case 0xC0 and 0xC1 + Result := False + elseif (c & 0xE0) = 0xC0 then + -- 110xxxxx 10xxxxxx + i := i + 1 + if i <= n then + c2 := s.code (i) + if + (c2 & 0xC0) = 0x80 + then + w := ((c & 0x1F) |<< 6) + | (c2 & 0x3F) + Result := 0x80 <= w and w <= 0x7FF + else + Result := False + end + else + l_is_incomplete_stream := True + end + elseif (c & 0xF0) = 0xE0 then + -- 1110xxxx 10xxxxxx 10xxxxxx + i := i + 2 + if i <= n then + c2 := s.code (i - 1) + c3 := s.code (i) + if + (c2 & 0xC0) = 0x80 and + (c3 & 0xC0) = 0x80 + then + w := ((c & 0xF) |<< 12) + | ((c2 & 0x3F) |<< 6) + | (c3 & 0x3F) + if 0x800 <= w and w <= 0xFFFF then + if 0xD800 <= w and w <= 0xDFFF then + -- The definition of UTF-8 prohibits encoding character numbers between U+D800 and U+DFFF + Result := False + end + else + Result := False + end + else + Result := False + end + else + if i - 1 <= n then + Result := (s.code (i - 1) & 0xC0) = 0x80 + end + l_is_incomplete_stream := True + end + elseif (c & 0xF8) = 0xF0 then -- 0001 0000-0010 FFFF + -- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if 0xF5 <= c and c <= 0xFF then + -- The octet values C0, C1, F5 to FF never appear. + Result := False + else + i := i + 3 + if i <= n then + c2 := s.code (i - 2) + c3 := s.code (i - 1) + c4 := s.code (i) + if + (c2 & 0xC0) = 0x80 and + (c3 & 0xC0) = 0x80 and + (c4 & 0xC0) = 0x80 + then + w := ((c & 0x7) |<< 18) | + ((c2 & 0x3F) |<< 12) | + ((c3 & 0x3F) |<< 6) | + (c4 & 0x3F) + Result := 0x1_0000 <= w and w <= 0x10_FFFF + else + Result := False + end + else + if i - 2 <= n then + c2 := s.code (i - 2) + Result := (c2 & 0xC0) = 0x80 + if Result then + if c = 0xF4 and c2 >= 0x90 then + --| any byte 10xxxxxx (i.e >= 0x80) that would come after, + -- will result in out of range code point + -- indeed 0xF4 0x90 0x80 0x80 = 0x1100 0000 > 0x10_FFFF + Result := False + elseif i - 1 <= n then + Result := (s.code (i - 1) & 0xC0) = 0x80 + end + end + end + l_is_incomplete_stream := True + end + end + else + -- Invalid byte in UTF-8 + Result := False + end + if Result then + if l_is_incomplete_stream then + Result := a_is_stream + elseif a_is_stream then + last_utf_8_stream_validation_position := i + end + end + end + end +end diff --git a/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e b/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e new file mode 100644 index 00000000..b1c4b8d8 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e @@ -0,0 +1,754 @@ +note + description: "Summary description for {WEB_SOCKET}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + WEB_SOCKET + +inherit + WGI_STANDALONE_CONNECTOR_ACCESS + + HTTPD_LOGGER_CONSTANTS + + WEB_SOCKET_CONSTANTS + + SHARED_BASE64 + +create + make + +feature {NONE} -- Initialization + + make (req: WSF_REQUEST; res: WSF_RESPONSE) + do + request := req + response := res + is_verbose := True + + if + attached {WGI_STANDALONE_INPUT_STREAM} req.input as r_input + then + socket := r_input.source + else + create socket.make_empty + check has_socket: False end + end + end + +feature -- Access + + socket: HTTPD_STREAM_SOCKET + + request: WSF_REQUEST + + response: WSF_RESPONSE + +feature -- Access + + is_websocket: BOOLEAN + + has_error: BOOLEAN + + is_verbose: BOOLEAN + + socket_is_ready_for_reading: BOOLEAN + do + Result := socket.ready_for_reading + end + +feature -- Element change + + log (m: READABLE_STRING_8; lev: INTEGER) + do + if is_verbose then + response.put_error (m) + end + end + +feature -- Basic Operation + + open_ws_handshake + -- The opening handshake is intended to be compatible with HTTP-based + -- server-side software and intermediaries, so that a single port can be + -- used by both HTTP clients alking to that server and WebSocket + -- clients talking to that server. To this end, the WebSocket client's + -- handshake is an HTTP Upgrade request: + + -- GET /chat HTTP/1.1 + -- Host: server.example.com + -- Upgrade: websocket + -- Connection: Upgrade + -- Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== + -- Origin: http://example.com + -- Sec-WebSocket-Protocol: chat, superchat + -- Sec-WebSocket-Version: 13 + local + l_sha1: SHA1 + l_key : STRING + l_handshake: STRING + req: like request + res: like response + do + req := request + res := response + is_websocket := False + has_error := False + + -- Reading client's opening GT + + -- TODO extract to a validator handshake or something like that. + if is_verbose then + log ("%NReceive <====================", debug_level) + if attached req.raw_header_data as rhd then + log (rhd, debug_level) + end + end + if + req.is_get_request_method and then -- MUST be GET request! + attached req.meta_string_variable ("HTTP_UPGRADE") as l_upgrade_key and then + l_upgrade_key.is_case_insensitive_equal_general ("websocket") -- Upgrade header must be present with value websocket + then + is_websocket := True +-- if +-- attached {WGI_STANDALONE_INPUT_STREAM} req.input as r_input and then +-- attached r_input.source as l_socket +-- then +-- l_socket.set_blocking +-- end + socket.set_blocking + if + attached req.meta_string_variable ("HTTP_SEC_WEBSOCKET_KEY") as l_ws_key and then -- Sec-websocket-key must be present + attached req.meta_string_variable ("HTTP_CONNECTION") as l_connection_key and then -- Connection header must be present with value Upgrade + l_connection_key.has_substring ("Upgrade") and then + attached req.meta_string_variable ("HTTP_SEC_WEBSOCKET_VERSION") as l_version_key and then -- Version header must be present with value 13 + l_version_key.is_case_insensitive_equal ("13") and then + attached req.http_host -- Host header must be present + then + if is_verbose then + log ("key " + l_ws_key, debug_level) + end + -- Sending the server's opening handshake + create l_sha1.make + l_sha1.update_from_string (l_ws_key + magic_guid) + l_key := Base64_encoder.encoded_string (digest (l_sha1)) +-- create l_handshake.make_from_string ("") --HTTP/1.1 101 Switching Protocols%R%N") + create l_handshake.make_from_string ("HTTP/1.1 101 Switching Protocols%R%N") + l_handshake.append_string ("Upgrade: websocket%R%N") + l_handshake.append_string ("Connection: Upgrade%R%N") + l_handshake.append_string ("Sec-WebSocket-Accept: ") + l_handshake.append_string (l_key) + l_handshake.append_string ("%R%N") + -- end of header empty line +--not with WSF_RESPONSE l_handshake.append_string ("%R%N") + l_handshake.append_string ("%R%N") + if is_verbose then + log ("%N================> Send", debug_level) + log (l_handshake, debug_level) + end + socket.put_string (l_handshake) +-- res.set_status_code_with_reason_phrase (101, "Switching Protocols") +-- res.put_header_text (l_handshake) + else + has_error := True + if is_verbose then + log ("Error (opening_handshake)!!!", debug_level) + end + -- If we cannot complete the handshake, then the server MUST stop processing the client's handshake and return an HTTP response with an + -- appropriate error code (such as 400 Bad Request). + res.set_status_code_with_reason_phrase (400, "Bad Request") +-- a_socket.put_string ("HTTP/1.1 400 Bad Request%N") + end + else + is_websocket := False + end + end + +feature -- Response! + + 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 + do + print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N") + 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 ((0 | 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 ((0 | 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 (n.as_natural_32) + end + socket.put_string (l_header_message) + + l_chunk_size := 16_384 -- 16K + if l_message_count < l_chunk_size then + socket.put_string (a_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 := a_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 + + next_frame: 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 + socket_in_blocking_mode: socket.is_blocking + local + l_socket: like socket + l_opcode: INTEGER + l_len: INTEGER + l_remaining_len: INTEGER + l_payload_len: NATURAL_64 + l_masking_key: detachable READABLE_STRING_8 + 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 +-- l_input := request.input + l_socket := socket + 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 (l_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 + ")", debug_level) + 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 + ")", debug_level) + 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 (l_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 (l_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 (l_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 + l_masking_key := next_bytes (l_socket, 4) -- 32 bits + debug ("ws") + print (" Masking key bits=" + string_to_byte_representation (l_masking_key)) + io.put_new_line + end + if l_masking_key.count < 4 then + debug ("ws") + print ("masking-key read stream -> " + l_socket.bytes_read.out + " bits%N") + end + Result.report_error (Invalid_data, "Incomplete data for Masking-key") + l_masking_key := Void + end + else + Result.report_error (protocol_error, "All frames sent from client to server are masked!") + 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 +-- l_input.read_string (l_chunk_size) + l_socket.read_stream (l_chunk_size) +-- l_bytes_read := l_input.last_string.count + l_bytes_read := l_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 l_bytes_read > 0 then + l_remaining_len := l_remaining_len - l_bytes_read + l_chunk := l_socket.last_string + if l_masking_key /= Void then + -- Masking + -- http://tools.ietf.org/html/rfc6455#section-5.3 + unmask (l_chunk, l_fetch_count + 1, l_masking_key) + else + check + client_frame_should_always_be_encoded: False + end + end + 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 <===============", debug_level) + 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, debug_level) + 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 + has_error := Result = Void or else Result.has_error + rescue + retried := True + if Result /= Void then + Result.report_error (internal_error, "Internal error") + end + retry + end + + +feature -- Encoding + + digest (a_sha1: SHA1): STRING + -- Digest of `a_sha1'. + -- Should by in SHA1 class + local + l_digest: SPECIAL [NATURAL_8] + index, l_upper: INTEGER + do + l_digest := a_sha1.digest + create Result.make (l_digest.count // 2) + from + index := l_digest.Lower + l_upper := l_digest.upper + until + index > l_upper + loop + Result.append_character (l_digest [index].to_character_8) + index := index + 1 + end + end + +feature {NONE} -- Socket helpers + + next_bytes (a_socket: HTTPD_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 + +feature -- Masking Data Client - Server + + 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 + + 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 + + +note + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/server/wsf/connector/standalone_websocket/wgi_standalone_websocket_connector.e b/library/server/wsf/connector/standalone_websocket/wgi_standalone_websocket_connector.e new file mode 100644 index 00000000..15675200 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/wgi_standalone_websocket_connector.e @@ -0,0 +1,44 @@ +note + description: "Summary description for {WGI_STANDALONE_WEBSOCKET_CONNECTOR}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + WGI_STANDALONE_WEBSOCKET_CONNECTOR [G -> WGI_EXECUTION create make end] + +inherit + WGI_STANDALONE_CONNECTOR [G] + redefine + name, version + end + +create + make, + make_with_base + +feature -- Access + + name: STRING_8 + -- Name of Current connector + once + Result := "ws_httpd" + end + + version: STRING_8 + -- Version of Current connector + once + Result := "1.0" + end + +note + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/library/server/wsf/connector/standalone_websocket/wsf_standalone_websocket_service_launcher.e b/library/server/wsf/connector/standalone_websocket/wsf_standalone_websocket_service_launcher.e new file mode 100644 index 00000000..0a334fb0 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/wsf_standalone_websocket_service_launcher.e @@ -0,0 +1,49 @@ +note + description: "[ + Component to launch the service using the default connector + + Eiffel Web httpd for this class + + + The httpd default connector support options: + port: numeric such as 8099 (or equivalent string as "8099") + base: base_url (very specific to standalone server) + verbose: to display verbose output, useful for standalone connector + force_single_threaded: use only one thread, useful for standalone connector + + check WSF_SERVICE_LAUNCHER for more documentation + ]" + date: "$Date$" + revision: "$Revision$" + +class + WSF_STANDALONE_WEBSOCKET_SERVICE_LAUNCHER [G -> WSF_WEBSOCKET_EXECUTION create make end] + +inherit + WSF_STANDALONE_SERVICE_LAUNCHER [G] + redefine + connector + end + +create + make, + make_and_launch + +feature -- Status report + + connector: WGI_STANDALONE_WEBSOCKET_CONNECTOR [G] + -- Default connector + +;note + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" + +end + diff --git a/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e b/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e new file mode 100644 index 00000000..0505795d --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e @@ -0,0 +1,240 @@ +note + description: "[ + Objects that ... + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +deferred class + WSF_WEBSOCKET_EXECUTION + +inherit + WSF_EXECUTION + rename + execute as http_execute + end + + WEB_SOCKET_CONSTANTS + + REFACTORING_HELPER + + HTTPD_LOGGER_CONSTANTS + + WGI_STANDALONE_CONNECTOR_ACCESS + +--create +-- make + +feature -- Execution + + is_verbose: BOOLEAN + + is_websocket: BOOLEAN + + has_error: BOOLEAN + + log (m: READABLE_STRING_8; lev: INTEGER) + do + + end + + http_execute + local + ws: WEB_SOCKET + do + has_error := False + is_websocket := False + create ws.make (request, response) + ws.open_ws_handshake + if ws.is_websocket then + has_error := ws.has_error + is_websocket := True + on_open (ws) + execute_websocket (ws) + else + execute + end + end + + execute_websocket (ws: WEB_SOCKET) + require + is_websocket: is_websocket + local + exit: BOOLEAN + l_frame: detachable WEB_SOCKET_FRAME + l_client_message: detachable READABLE_STRING_8 + l_utf: UTF_CONVERTER + do + from + -- loop until ws is closed or has error. + until + has_error or else exit + loop +-- debug ("dbglog") +-- dbglog (generator + ".LOOP WS_REQUEST_HANDLER.process_request {...}") +-- end + if ws.socket_is_ready_for_reading then + l_frame := ws.next_frame + 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. + on_event (ws, ic.item.payload_data, ic.item.opcode) + exit := True + elseif ic.item.is_ping then + -- FIXME reply only to the most recent ping ... + on_event (ws, ic.item.payload_data, ic.item.opcode) + else + on_event (ws, ic.item.payload_data, ic.item.opcode) + end + end + end + + l_client_message := l_frame.payload_data + if l_client_message = Void then + l_client_message := "" + end + +-- debug ("ws") + if is_verbose then + 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 + on_event (ws, l_client_message, l_frame.opcode) + exit := True + elseif l_frame.is_binary then + on_event (ws, l_client_message, l_frame.opcode) + elseif l_frame.is_text then + check is_valid_utf_8: l_utf.is_valid_utf_8_string_8 (l_client_message) end + on_event (ws, l_client_message, l_frame.opcode) + else + on_event (ws, l_client_message, l_frame.opcode) + end + else +-- debug ("ws") + if is_verbose then + 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 + on_event (ws, "", connection_close_frame) + exit := True + end + else + if is_verbose then + log (generator + ".WAITING WS_REQUEST_HANDLER.process_request {..}", debug_level) + end + end + end + end + + execute + -- Execute Current request, + -- getting data from `request' + -- and response to client via `response'. + deferred + end + +feature -- Web Socket Interface + + on_event (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8; a_opcode: INTEGER) + -- Called when a frame from the client has been receive + local + l_message: READABLE_STRING_8 + do + debug ("ws") + print ("%Non_event (conn, a_message, " + opcode_name (a_opcode) + ")%N") + end + if a_message = Void then + create {STRING} l_message.make_empty + else + l_message := a_message + end + + if a_opcode = Binary_frame then + on_binary (ws, l_message) + elseif a_opcode = Text_frame then + on_text (ws, l_message) + elseif a_opcode = Pong_frame then + on_pong (ws, l_message) + elseif a_opcode = Ping_frame then + on_ping (ws, l_message) + elseif a_opcode = Connection_close_frame then + on_connection_close (ws, "") + else + on_unsupported (ws, l_message, a_opcode) + end + end + + on_open (ws: WEB_SOCKET) + -- Called after handshake, indicates that a complete WebSocket connection has been established. + deferred + end + + on_binary (ws: WEB_SOCKET; a_message: READABLE_STRING_8) + deferred + end + + on_pong (ws: WEB_SOCKET; a_message: READABLE_STRING_8) + do + -- log ("Its a pong frame") + -- at first we ignore pong + -- FIXME: provide better explanation + end + + on_ping (ws: WEB_SOCKET; a_message: READABLE_STRING_8) + do + ws.send (Pong_frame, a_message) + end + + on_text (ws: WEB_SOCKET; a_message: READABLE_STRING_8) + deferred + end + + on_unsupported (ws: WEB_SOCKET; a_message: READABLE_STRING_8; a_opcode: INTEGER) + do + -- do nothing + fixme ("implement on_unsupported") + end + + on_connection_close (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8) + do + ws.send (Connection_close_frame, "") + end + + on_close (ws: WEB_SOCKET) + -- Called after the WebSocket connection is closed. + deferred + end + + +note + copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end