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:
2016-10-13 16:25:11 +02:00
parent 981942b2d6
commit 897f64e4fe
54 changed files with 2233 additions and 95 deletions

View 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