WebSocket compression update with new classes to

parse the compression header.
This commit is contained in:
jvelilla
2016-10-17 10:15:12 -03:00
parent f12158e535
commit 82c3e2aebb
6 changed files with 491 additions and 10 deletions

View File

@@ -0,0 +1,111 @@
note
description: "{COMPRESSION_EXTENSIONS_PARSER} Parse the SEC_WEBSOCKET_EXTENSION header as par of websocket opening handshake."
date: "$Date$"
revision: "$Revision$"
EIS: "name=Compression Extension for WebSocket"
class
COMPRESSION_EXTENSIONS_PARSER
create
make
feature {NONE} -- Initialize
make (a_header: STRING_32)
do
header := a_header
create {ARRAYED_LIST [WEBSOCKET_PCME]} last_offers.make (0)
ensure
header_set: header = a_header
end
feature -- Access
header: STRING_32
-- Content raw header `Sec-Websocket-Extensions'.
last_offers: LIST [WEBSOCKET_PCME]
-- List of potential offered PMCE
--| From Sec-Websocket-Extensions header.
--| The order of elements is important as it specifies the client's preferences.
feature -- Parse
parse
-- Parse `SEC-WEBSOCKET-EXTENSIONS' header.
-- The result is available in `last_offer'.
local
l_offers: ARRAYED_LIST [WEBSOCKET_PCME]
l_pcme: WEBSOCKET_PCME
do
create l_offers.make (1)
if attached header.split (',') as l_list then
-- Multiple offers separated by ',', if l_list.count > 1
across l_list as ic loop
-- Shared code extract to an external feature.
l_offers.force (parse_parameters (ic.item))
end
else
-- we should raise an Issue.
end
last_offers := l_offers
end
feature {NONE}-- Parse Compression Extension.
parse_parameters (a_string: STRING_32): WEBSOCKET_PCME
local
l_first: BOOLEAN
l_str: STRING_32
l_key: STRING_32
l_value: STRING_32
do
if attached a_string.split (';') as l_parameters then
-- parameters for the current offer.
create Result
across
l_parameters as ip
from
l_first := True
loop
if l_first then
l_str := ip.item
l_str.adjust
Result.set_name (l_str)
l_first := False
else
l_str := ip.item
l_str.adjust
if l_str.has ('=') then
-- The parameter has a value
-- server_max_window_bits = 10
l_key := l_str.substring (1, l_str.index_of ('=', 1) - 1) -- Exclude =
l_value := l_str.substring (l_str.index_of ('=', 1) + 1, l_str.count) -- Exclude =
Result.force (l_value, l_key)
else
Result.force (Void, l_str)
end
end
end
else
-- Compression Extension simple
--| like
--| permessage-deflate
l_str := a_string
l_str.adjust
create Result
Result.set_name (l_str)
end
end
note
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -0,0 +1,65 @@
note
description: "[Object that validate a PMCE permessage defalate extension,
using the DEFLATE algorithm
]"
date: "$Date$"
revision: "$Revision$"
class
WEB_SOCKET_PMCE_DEFLATE_VALIDATOR
feature -- Validate
feature -- Access
name: STRING = "permessage-deflate"
-- registered extension name.
parameters: STRING_TABLE [BOOLEAN]
-- extension parameters
note
EIS: "name=Extension Parameters", "src=https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-28#section-7.1", "protocol=url"
once
create Result.make_caseless (4)
Result.force (False, "server_no_context_takeover")
Result.force (False, "client_no_context_takeover")
Result.force (True, "server_max_windows_bits")
Result.force (True, "client_max_windows_bits")
end
sliding_windows_size: STRING_TABLE [INTEGER]
-- LZ77 sliding window size.
--! Map with valid windows and the
--! context parameter, and integer value
--! between 8 and 15.
note
EIS:"name=Limiting the LZ77 sliding window size", "src=https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-28#section-7.1.2", "protocol=url"
once
create Result.make (7)
Result.force (256, "8")
Result.force (512, "9")
Result.force (1024, "10")
Result.force (2048, "11")
Result.force (4096, "12")
Result.force (8192, "13")
Result.force (16384, "14")
Result.force (32768, "15")
end
note
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -0,0 +1,52 @@
note
description: "{WEBSOCKET_PCME}, object that represent a websocket per-message compression extension."
date: "$Date$"
revision: "$Revision$"
class
WEBSOCKET_PCME
feature -- Access
name: detachable STRING_32
-- Compression extension name.
parameters: detachable STRING_TABLE [detachable STRING_32]
-- Compression extensions parameter.
feature -- Change Element
set_name (a_name: STRING_32)
-- Set name with `a_name'.
do
name := a_name
ensure
name_set: name = a_name
end
force (a_value: detachable STRING_32; a_key: STRING_32)
-- Update table `parameters' so that `a_value' will be the item associated
-- with `a_key'.
local
l_parameters: like parameters
do
l_parameters := parameters
if attached l_parameters then
l_parameters.force (a_value, a_key)
else
create l_parameters.make_caseless (1)
l_parameters.force (a_value, a_key)
end
parameters := l_parameters
end
note
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -93,6 +93,9 @@ feature -- Access
is_fin: BOOLEAN
-- is the final fragment in a message?
is_rsv1: BOOLEAN
-- is extension negotiation in a message?
fragment_count: INTEGER
payload_length: NATURAL_64
@@ -132,6 +135,11 @@ feature -- Operation
is_fin := a_flag_is_fin
end
update_rsv1 (a_flag_rsv1: BOOLEAN)
do
is_rsv1 := a_flag_rsv1
end
feature {WEB_SOCKET_FRAME} -- Change: injected control frames
add_injected_control_frame (f: WEB_SOCKET_FRAME)
@@ -434,4 +442,14 @@ feature {NONE} -- Helper
end
end
end
note
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -44,6 +44,11 @@ feature {NONE} -- Initialization
end
end
set_pcme_deflate
do
end
feature -- Access
socket: HTTPD_STREAM_SOCKET
@@ -114,6 +119,14 @@ feature -- Element change
verbose_level := lev
end
mark_pcme_supported
-- Set the websocket to handle pcme.
do
is_pcme_supported := True
ensure
pmce_supported_true: is_pcme_supported
end
feature -- Basic operation
put_error (a_message: READABLE_STRING_8)
@@ -142,10 +155,13 @@ feature -- Basic Operation
-- Host: server.example.com
-- Upgrade: websocket
-- Connection: Upgrade
--! Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
-- Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
-- Origin: http://example.com
-- Sec-WebSocket-Protocol: chat, superchat
-- Sec-WebSocket-Version: 13
note
EIS: "name=Compression Extensions for WebSocket", "src=https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-28", "protocol=url"
local
l_sha1: SHA1
l_key : STRING
@@ -155,6 +171,7 @@ feature -- Basic Operation
-- Reset values.
is_websocket := False
has_error := False
on_handshake:= True
-- Local cache.
req := request
@@ -184,9 +201,20 @@ feature -- Basic Operation
l_version_key.is_case_insensitive_equal ("13") and then
attached req.http_host -- Host header must be present
then
-- here we can check for Sec-WebSocket-Extensions, it could be a collection of extensions.
if is_pcme_supported and then attached req.meta_string_variable ("HTTP_SEC_WEBSOCKET_EXTENSIONS") as l_ws_extension then
-- at the moment we only handle web socket compression extension (PMCE permessage-deflate).
--| We need a way to define which compression algorithm the server support.
--|
handle_extensions (l_ws_extension)
end
if is_verbose then
log ("key " + l_ws_key, debug_level)
end
--! Before to send the response we need to check the Sec-Web-Socket extension.
--! We can write the compression extension here or just build
--! a set of classes and call them where it's needed.
-- Sending the server's opening handshake
create l_sha1.make
l_sha1.update_from_string (l_ws_key + magic_guid)
@@ -195,6 +223,13 @@ feature -- Basic Operation
res.header.add_header_key_value ("Connection", "Upgrade")
res.header.add_header_key_value ("Sec-WebSocket-Accept", l_key)
-- Sec-WebSocket-Extensions
if is_pcme_supported and then attached accepted_offer as l_offer
and then attached extension_response(l_offer) as l_extension_response
then
res.header.add_header_key_value ("Sec-WebSocket-Extensions", l_extension_response)
end
if is_verbose then
log ("%N================> Send Handshake", debug_level)
if attached {HTTP_HEADER} res.header as h then
@@ -228,14 +263,25 @@ feature -- Response!
l_message_count: INTEGER
n: NATURAL_64
retried: BOOLEAN
l_message: STRING_8
l_opcode: NATURAL_32
do
l_message := a_message
if attached accepted_offer and then not on_handshake then
l_message := compress_string (l_message)
end
debug ("ws")
print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N")
end
if not retried then
create l_header_message.make_empty
if attached accepted_offer and then not on_handshake then
l_opcode := (0x80 | a_opcode).to_natural_32
l_header_message.append_code ((l_opcode.bit_xor (0b1000000)))
else
l_header_message.append_code ((0x80 | a_opcode).to_natural_32)
l_message_count := a_message.count
end
l_message_count := l_message.count
n := l_message_count.to_natural_64
if l_message_count > 0xffff then
--! Improve. this code needs to be checked.
@@ -259,7 +305,7 @@ feature -- Response!
l_chunk_size := 16_384 -- 16K TODO: see if we should make it customizable.
if l_message_count < l_chunk_size then
socket.put_string (a_message)
socket.put_string (l_message)
else
from
i := 0
@@ -269,7 +315,7 @@ feature -- Response!
debug ("ws")
print ("Sending chunk " + (i + 1).out + " -> " + (i + l_chunk_size).out +" / " + l_message_count.out + "%N")
end
l_chunk := a_message.substring (i + 1, l_message_count.min (i + l_chunk_size))
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
@@ -285,7 +331,7 @@ feature -- Response!
end
rescue
retried := True
io.put_string ("Internal error in " + generator + ".do_send (conn, a_opcode=" + a_opcode.out + ", a_message) !%N")
io.put_string ("Internal error in " + generator + ".do_send (conn, a_opcode=" + a_opcode.out + ", l_message) !%N")
retry
end
@@ -341,6 +387,7 @@ feature -- Response!
s: STRING
is_data_frame_ok: BOOLEAN -- Is the last process data framing ok?
retried: BOOLEAN
l_frame_rsv: INTEGER
do
if not retried then
l_socket := socket
@@ -368,6 +415,7 @@ feature -- Response!
end
l_fin := l_byte & (0b10000000) /= 0
l_rsv := l_byte & (0b01110000) = 0
l_frame_rsv := (l_byte & 0x70) |>> 4
l_opcode := l_byte & 0b00001111
if Result /= Void then
if l_opcode = Result.opcode then
@@ -402,7 +450,15 @@ feature -- Response!
end
-- rsv validation
if not l_rsv then
if l_frame_rsv /= 0 then
if attached accepted_offer and then l_frame_rsv = 4 then
if Result.is_continuation then
Result.report_error (protocol_error, "RSV is set and no extension is negotiated")
else
on_handshake := False
Result.update_rsv1 (True)
end
elseif not l_rsv then
-- RSV1, RSV2, RSV3: 1 bit each
-- MUST be 0 unless an extension is negotiated that defines meanings
@@ -414,6 +470,7 @@ feature -- Response!
-- 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
end
else
if is_verbose then
log ("+ INVALID frame " + opcode_name (l_opcode) + " (fin=" + l_fin.out + ")", debug_level)
@@ -538,7 +595,13 @@ feature -- Response!
end
end
l_fetch_count := l_fetch_count + l_bytes_read
if attached accepted_offer then
-- Uncompress data.
Result.append_payload_data_chop (uncompress_string (l_chunk), l_bytes_read, l_remaining_len = 0)
else
Result.append_payload_data_chop (l_chunk, l_bytes_read, l_remaining_len = 0)
end
else
Result.report_error (internal_error, "Issue reading payload data...")
end
@@ -792,7 +855,166 @@ feature {NONE} -- Debug
end
note
feature -- PCME
uncompress_string (a_string: STRING): STRING
local
di: ZLIB_STRING_UNCOMPRESS
l_string: STRING
l_array: ARRAY [NATURAL_8]
l_byte: SPECIAL [INTEGER_8]
do
create l_string.make_from_string (a_string)
--Prepend 0x78 and 09c
l_string.prepend_character ((156).to_character_8)
l_string.prepend_character ((120).to_character_8)
-- Append 4 octects 0x00 0x00 0xff 0xff to the tail of the paiload message
l_string.append_character ((0x00).to_character_8)
l_string.append_character ((0x00).to_character_8)
l_string.append_character ((0xff).to_character_8)
l_string.append_character ((0xff).to_character_8)
l_array := string_to_array (l_string)
l_byte := byte_array (l_array)
create di.string_stream (l_string)
Result := di.to_string
debug ("ws")
print ("%NBytes uncompresses:" + di.total_bytes_uncompressed.out)
print ("%NUncompress message:" + Result)
end
end
compress_string (a_string: STRING): STRING
local
dc: ZLIB_STRING_COMPRESS
l_string: STRING
do
create Result.make_empty
create dc.string_stream (Result)
dc.mark_sync_flush
dc.put_string (a_string)
Result := Result.substring (3, Result.count - 4)
debug ("ws")
print ("%NBytes uncompresses:" + dc.total_bytes_compressed.out )
end
end
byte_array (a_bytes: SPECIAL [NATURAL_8]) : SPECIAL [INTEGER_8]
local
i: INTEGER
do
create Result.make_filled (0,a_bytes.count)
across a_bytes as c
loop
Result.put(to_byte(c.item.as_integer_8), i)
i := i + 1
end
end
to_byte (a_val : INTEGER) : INTEGER_8
-- takes a value between 0 and 255
-- Result :-128 to 127
do
if a_val >= 128 then
Result := (-256 + a_val).to_integer_8
else
Result := a_val.to_integer_8
end
ensure
result_value : 127 >= Result and Result >= -128
end
string_to_array (s: STRING): ARRAY [NATURAL_8]
local
i, n: INTEGER
c: INTEGER
do
n := s.count
create Result.make_empty
if n > 0 then
from
i := 1
until
i > n
loop
c := s [i].code
check
c <= 0xFF
end
Result.force (c.as_natural_8, i)
i := i + 1
end
end
end
feature {NONE} -- Extensions
is_pcme_supported: BOOLEAN
--| Temporary hack to test websocket compression
on_handshake: BOOLEAN
permessage_compression: STRING = "permessage-deflate"
--| Temporary hack to test websocket compression
extension_response (a_offer: WEBSOCKET_PCME): detachable STRING_8
do
if attached a_offer.name as l_name then
create Result.make_from_string (l_name)
end
end
handle_extensions (a_extension: READABLE_STRING_32)
-- handle WebSocket extensions.
local
l_parse: COMPRESSION_EXTENSIONS_PARSER
l_offers: LIST [WEBSOCKET_PCME]
l_accepted: BOOLEAN
l_offer: WEBSOCKET_PCME
do
-- TODO improve handle
-- at the moment only check we have permessage_compression
--| TODO add validation and select the best offer.
create l_parse.make (a_extension)
l_parse.parse
l_offers := l_parse.last_offers
if not l_offers.is_empty then
-- filter by permessage-deflate.
--| TODO: validate if it's a valid extension.
--| validate params.
across l_offers as ic
until
l_accepted
loop
if attached {STRING_32} ic.item.name as l_name and then
l_name.is_case_insensitive_equal_general (permessage_compression)
then
l_accepted := True
create l_offer
l_offer.set_name (permessage_compression)
end
end
accepted_offer := l_offer
end
end
accepted_offer: detachable WEBSOCKET_PCME
-- Accepted compression extension.
;note
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[

View File

@@ -29,6 +29,7 @@ feature -- Execution
ws_h: like new_websocket_handler
do
create ws.make (request, response)
initialize_websocket_options (ws)
ws.open_ws_handshake
if ws.is_websocket then
if ws.has_error then
@@ -51,6 +52,18 @@ feature -- Execution
deferred
end
feature -- WebSocket Options
initialize_websocket_options (ws: WEB_SOCKET)
-- Set web socket options (extensions) to be used as part of the ws opem handshake
--| for example set pcme algorithm etc.
--| Other option is create a new Class WEB_SOCKET_OPTION/ or
--| WEB_SOCKET_EXTENTIONS
--| defining all potenial extensions to the protocol.
do
-- To be redefined
end
feature -- Factory
new_websocket_handler (ws: WEB_SOCKET): WEB_SOCKET_HANDLER