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_secure implies port = wss_port_default port_ws: not is_secure 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_secure implies port = wss_port_default port_ws: not is_secure 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_secure 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