Reuse http_network library.
Reintroduced HTTPD_STREAM_SOCKET for backward compatibility, and ease of usage. Added websocket libraries (client, and protocol).
This commit is contained in:
@@ -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
|
||||
54
library/network/websocket/client/src/ssl/web_socket_client.e
Normal file
54
library/network/websocket/client/src/ssl/web_socket_client.e
Normal file
@@ -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
|
||||
100
library/network/websocket/client/src/web_socket.e
Normal file
100
library/network/websocket/client/src/web_socket.e
Normal file
@@ -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
|
||||
604
library/network/websocket/client/src/web_socket_client_i.e
Normal file
604
library/network/websocket/client/src/web_socket_client_i.e
Normal file
@@ -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
|
||||
@@ -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
|
||||
739
library/network/websocket/client/src/web_socket_impl.e
Normal file
739
library/network/websocket/client/src/web_socket_impl.e
Normal file
@@ -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
|
||||
@@ -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
|
||||
120
library/network/websocket/client/src/web_socket_ready_state.e
Normal file
120
library/network/websocket/client/src/web_socket_ready_state.e
Normal file
@@ -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
|
||||
61
library/network/websocket/client/src/web_socket_subscriber.e
Normal file
61
library/network/websocket/client/src/web_socket_subscriber.e
Normal file
@@ -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
|
||||
45
library/network/websocket/client/web_socket_client-safe.ecf
Normal file
45
library/network/websocket/client/web_socket_client-safe.ecf
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-13-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-13-0 http://www.eiffel.com/developers/xml/configuration-1-13-0.xsd" name="web_socket_client" uuid="EE010507-597F-4FAD-8EFA-B7251E800911" library_target="web_socket_client">
|
||||
<target name="web_socket_client">
|
||||
<root all_classes="true"/>
|
||||
<file_rule>
|
||||
<exclude>/.git$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/CVS$</exclude>
|
||||
<exclude>/.svn$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true" void_safety="all">
|
||||
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<setting name="concurrency" value="thread"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto-safe.ecf"/>
|
||||
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder-safe.ecf"/>
|
||||
<library name="net" location="$ISE_LIBRARY\library\net\net-safe.ecf"/>
|
||||
<library name="thread" location="$ISE_LIBRARY\library\thread\thread-safe.ecf"/>
|
||||
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri-safe.ecf"/>
|
||||
<library name="lib_http_network" location="..\..\http_network\http_network-safe.ecf"/>
|
||||
<library name="lib_web_socket_protocol" location="..\protocol\web_socket_protocol-safe.ecf"/>
|
||||
<cluster name="web_socket_client" location=".\src\" recursive="true">
|
||||
<file_rule>
|
||||
<exclude>/no_ssl$</exclude>
|
||||
<exclude>/ssl$</exclude>
|
||||
<exclude>/spec$</exclude>
|
||||
</file_rule>
|
||||
<cluster name="ssl" location="$|ssl\" recursive="true">
|
||||
<condition>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<custom name="net_ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
<cluster name="no_ssl" location="$|no_ssl\" recursive="true">
|
||||
<condition>
|
||||
<custom name="ssl_enabled" excluded_value="true"/>
|
||||
<custom name="net_ssl_enabled" excluded_value="true"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
</cluster>
|
||||
</target>
|
||||
</system>
|
||||
64
library/network/websocket/client/web_socket_client.ecf
Normal file
64
library/network/websocket/client/web_socket_client.ecf
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-13-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-13-0 http://www.eiffel.com/developers/xml/configuration-1-13-0.xsd" name="web_socket_client" uuid="934F36F1-D417-4695-A5A9-2D005B35BB1B" library_target="web_socket_client">
|
||||
<target name="web_socket_client">
|
||||
<root all_classes="true"/>
|
||||
<file_rule>
|
||||
<exclude>/.git$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/CVS$</exclude>
|
||||
<exclude>/.svn$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true" void_safety="none">
|
||||
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<setting name="concurrency" value="thread"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf"/>
|
||||
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder.ecf"/>
|
||||
<library name="net" location="$ISE_LIBRARY\library\net\net.ecf"/>
|
||||
<library name="net_ssl" location="$ISE_LIBRARY\unstable\library\network\socket\netssl\net_ssl.ecf">
|
||||
<condition>
|
||||
<custom name="client_ssl_disabled" excluded_value="true"/>
|
||||
</condition>
|
||||
</library>
|
||||
<library name="thread" location="$ISE_LIBRARY\library\thread\thread.ecf"/>
|
||||
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri.ecf"/>
|
||||
<library name="lib_web_socket_protocol" location="..\protocol\web_socket_protocol.ecf"/>
|
||||
<cluster name="web_socket_client" location=".\src\" recursive="true">
|
||||
<file_rule>
|
||||
<exclude>/socket$</exclude>
|
||||
<exclude>/no_ssl$</exclude>
|
||||
<exclude>/ssl$</exclude>
|
||||
<exclude>/spec$</exclude>
|
||||
</file_rule>
|
||||
<cluster name="ssl" location="$|ssl\" recursive="true">
|
||||
<condition>
|
||||
<custom name="client_ssl_disabled" excluded_value="true"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
<cluster name="no_ssl" location="$|no_ssl\" recursive="true">
|
||||
<condition>
|
||||
<custom name="client_ssl_disabled" value="true"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
<cluster name="socket" location="$|socket\">
|
||||
<file_rule>
|
||||
<exclude>/tcp_stream_socket.e$</exclude>
|
||||
<condition>
|
||||
<version type="compiler" max="15.2.0.0"/>
|
||||
</condition>
|
||||
</file_rule>
|
||||
<cluster name="socket_ssl" location="$|ssl\" recursive="true" hidden="true">
|
||||
<condition>
|
||||
<custom name="client_ssl_disabled" excluded_value="true"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
</cluster>
|
||||
<cluster name="spec_before_15_01" location="$|spec\before_15_01\" recursive="true">
|
||||
<condition>
|
||||
<version type="compiler" max="15.2.0.0"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
</cluster>
|
||||
</target>
|
||||
</system>
|
||||
203
library/network/websocket/protocol/web_socket_constants.e
Normal file
203
library/network/websocket/protocol/web_socket_constants.e
Normal file
@@ -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
|
||||
35
library/network/websocket/protocol/web_socket_error_frame.e
Normal file
35
library/network/websocket/protocol/web_socket_error_frame.e
Normal file
@@ -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
|
||||
437
library/network/websocket/protocol/web_socket_frame.e
Normal file
437
library/network/websocket/protocol/web_socket_frame.e
Normal file
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="protocol" uuid="97582991-7BF3-4F4D-8944-8141ADE34274" library_target="protocol">
|
||||
<target name="protocol">
|
||||
<root all_classes="true"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||
<cluster name="src" location=".\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="protocol" uuid="97582991-7BF3-4F4D-8944-8141ADE34274" library_target="protocol">
|
||||
<target name="protocol">
|
||||
<root all_classes="true"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<cluster name="src" location=".\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
Reference in New Issue
Block a user