Renamed WGI_STANDALONE_CONNECTOR_ACCESS as WGI_STANDALONE_CONNECTOR_EXPORTER.

Isolate the websocket implementation in descendant of {WEB_SOCKET_EVENT_I}.
Added very simple echo websocket example.
+ code cleaning.
This commit is contained in:
2016-06-22 10:46:15 +02:00
parent b49e841ac7
commit 193cc3cbde
11 changed files with 595 additions and 340 deletions

View File

@@ -0,0 +1,29 @@
note
description : "simple application root class"
date : "$Date$"
revision : "$Revision$"
class
APPLICATION
create
make_and_launch
feature {NONE} -- Initialization
make_and_launch
local
l_launcher: WSF_STANDALONE_WEBSOCKET_SERVICE_LAUNCHER [APPLICATION_EXECUTION]
opts: WSF_SERVICE_LAUNCHER_OPTIONS
do
create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI} opts.make_from_file ("ws.ini")
create l_launcher.make_and_launch (options)
end
options: WSF_SERVICE_LAUNCHER_OPTIONS
-- Initialize current service.
do
create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI} Result.make_from_file ("ws.ini")
end
end

View File

@@ -0,0 +1,176 @@
note
description : "simple application execution"
date : "$Date$"
revision : "$Revision$"
class
APPLICATION_EXECUTION
inherit
WSF_WEBSOCKET_EXECUTION
WEB_SOCKET_EVENT_I
create
make
feature -- Basic operations
execute
local
s: STRING
dt: HTTP_DATE
do
-- To send a response we need to setup, the status code and
-- the response headers.
if request.path_info.same_string_general ("/app") then
s := websocket_app_html (9090)
else
s := "Hello World!"
create dt.make_now_utc
s.append (" (UTC time is " + dt.rfc850_string + ").")
s.append ("<p><a href=%"/app%">Websocket demo</a></p>")
end
response.put_header ({HTTP_STATUS_CODE}.ok, <<["Content-Type", "text/html"], ["Content-Length", s.count.out]>>)
response.set_status_code ({HTTP_STATUS_CODE}.ok)
response.header.put_content_type_text_html
response.header.put_content_length (s.count)
if attached request.http_connection as l_connection and then l_connection.is_case_insensitive_equal_general ("keep-alive") then
response.header.put_header_key_value ("Connection", "keep-alive")
end
response.put_string (s)
end
feature -- Websocket execution
new_websocket_handler (ws: WEB_SOCKET): WEB_SOCKET_HANDLER
do
create Result.make (ws, Current)
end
feature -- Websocket execution
on_open (ws: WEB_SOCKET)
do
ws.put_error ("Connecting")
end
on_binary (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
do
ws.send (Binary_frame, a_message)
end
on_text (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
do
-- Echo the message for testing.
ws.send (Text_frame, a_message)
end
on_close (ws: WEB_SOCKET)
-- Called after the WebSocket connection is closed.
do
ws.put_error ("Connection closed")
end
feature -- HTML Resource
websocket_app_html (a_port: INTEGER): STRING
do
Result := "[
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
var socket;
function connect(){
var host = "ws://127.0.0.1:##PORTNUMBER##";
try{
socket = new WebSocket(host);
message('<p class="event">Socket Status: '+socket.readyState);
socket.onopen = function(){
message('<p class="event">Socket Status: '+socket.readyState+' (open)');
}
socket.onmessage = function(msg){
message('<p class="message">Received: '+msg.data);
}
socket.onclose = function(){
message('<p class="event">Socket Status: '+socket.readyState+' (Closed)');
}
} catch(exception){
message('<p>Error'+exception);
}
}
function send(){
var text = $('#text').val();
if(text==""){
message('<p class="warning">Please enter a message');
return ;
}
try{
socket.send(text);
message('<p class="event">Sent: '+text)
} catch(exception){
message('<p class="warning">');
}
$('#text').val("");
}
function message(msg){
$('#chatLog').append(msg+'</p>');
}//End message()
$('#text').keypress(function(event) {
if (event.keyCode == '13') {
send();
}
});
$('#disconnect').click(function(){
socket.close();
});
if (!("WebSocket" in window)){
$('#chatLog, input, button, #examples').fadeOut("fast");
$('<p>Oh no, you need a browser that supports WebSockets. How about <a href="http://www.google.com/chrome">Google Chrome</a>?</p>').appendTo('#container');
}else{
//The user has WebSockets
connect();
}
});
</script>
<meta charset="utf-8" />
<style type="text/css">
body {font-family:Arial, Helvetica, sans-serif;}
#container { border:5px solid grey; width:800px; margin:0 auto; padding:10px; }
#chatLog { padding:5px; border:1px solid black; }
#chatLog p {margin:0;}
.event {color:#999;}
.warning { font-weight:bold; color:#CCC; }
</style>
<title>WebSockets Client</title>
</head>
<body>
<div id="wrapper">
<div id="container">
<h1>WebSockets Client</h1>
<div id="chatLog"></div>
<input id="text" type="text" />
<button id="disconnect">Disconnect</button>
</div>
</div>
</body>
</html>
]"
Result.replace_substring_all ("##PORTNUMBER##", a_port.out)
end
end

View File

@@ -0,0 +1,21 @@
<?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="websocket_app" uuid="75D17C20-10A8-4E4C-A059-33D72A2B6AEF">
<target name="websocket_app">
<root class="APPLICATION" feature="make_and_launch"/>
<file_rule>
<exclude>/.svn$</exclude>
<exclude>/CVS$</exclude>
<exclude>/EIFGENs$</exclude>
</file_rule>
<option warning="true" is_attached_by_default="true" void_safety="all" syntax="transitional">
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
</option>
<setting name="console_application" value="true"/>
<setting name="concurrency" value="scoop"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="http" location="..\..\library\network\protocol\http\http-safe.ecf"/>
<library name="standalone_websocket_connector" location="..\..\library\server\wsf\connector\standalone_websocket-safe.ecf" readonly="false"/>
<library name="wsf" location="..\..\library\server\wsf\wsf-safe.ecf"/>
<cluster name="app" location=".\" recursive="true"/>
</target>
</system>

View File

@@ -0,0 +1,8 @@
verbose=true
verbose_level=INFORMATION
port=9090
max_concurrent_connections=100
keep_alive_timeout=35
max_tcp_clients=100
socket_timeout=30000
max_keep_alive_requests=3000

View File

@@ -6,6 +6,6 @@ note
revision: "$Revision$" revision: "$Revision$"
deferred class deferred class
WGI_STANDALONE_CONNECTOR_ACCESS WGI_STANDALONE_CONNECTOR_EXPORTER
end end

View File

@@ -24,7 +24,7 @@ feature {NONE} -- Initialization
set_source (a_source) set_source (a_source)
end end
feature {WGI_STANDALONE_CONNECTOR, WGI_SERVICE, WGI_STANDALONE_CONNECTOR_ACCESS} -- Standalone feature {WGI_STANDALONE_CONNECTOR, WGI_SERVICE, WGI_STANDALONE_CONNECTOR_EXPORTER} -- Standalone
set_source (i: like source) set_source (i: like source)
do do

View File

@@ -0,0 +1,25 @@
<?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="wsf_standalone_websocket" uuid="7C83D4B4-39C9-4D27-941B-0F0AAD45122E" library_target="wsf_standalone_websocket">
<target name="wsf_standalone_websocket">
<root all_classes="true"/>
<file_rule>
<exclude>/EIFGENs$</exclude>
<exclude>/\.git$</exclude>
<exclude>/\.svn$</exclude>
</file_rule>
<option warning="true" full_class_checking="true" is_obsolete_routine_type="true" void_safety="none" syntax="provisional">
</option>
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
<library name="connector_standalone" location="..\..\ewsgi\connectors\standalone\standalone.ecf"/>
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf"/>
<library name="encoder" location="..\..\..\text\encoder\encoder.ecf" readonly="false"/>
<library name="error" location="..\..\..\utility\general\error\error.ecf"/>
<library name="ewsgi" location="..\..\ewsgi\ewsgi.ecf"/>
<library name="http" location="..\..\..\network\protocol\http\http.ecf"/>
<library name="httpd" location="..\..\ewsgi\connectors\standalone\lib\httpd\httpd.ecf"/>
<library name="time" location="$ISE_LIBRARY\library\time\time.ecf"/>
<library name="wsf" location="..\wsf.ecf"/>
<library name="wsf_standalone" location="standalone.ecf"/>
<cluster name="wsf_standalone_websocket" location=".\standalone_websocket\" recursive="true"/>
</target>
</system>

View File

@@ -1,7 +1,18 @@
note note
description: "[ description: "[
API to perform actions like opening and closing the connection, sending and receiving messages, and listening Websocket callback events for actions like opening and closing the connection,
for events. sending and receiving messages, and listening.
Define the websocket events:
- on_open
- on_binary
- on_text
- on_close
note: the following features could also be redefined:
- on_pong
- on_ping
- on_unsupported
]" ]"
date: "$Date$" date: "$Date$"
revision: "$Revision$" revision: "$Revision$"
@@ -16,16 +27,16 @@ inherit
feature -- Web Socket Interface feature -- Web Socket Interface
on_event (conn: HTTPD_STREAM_SOCKET; a_message: detachable READABLE_STRING_8; a_opcode: INTEGER) on_event (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8; a_opcode: INTEGER)
-- Called when a frame from the client has been receive -- Called when a frame from the client has been receive
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
local local
l_message: READABLE_STRING_8 l_message: READABLE_STRING_8
do do
debug ("ws") debug ("ws")
print ("%Non_event (conn, a_message, " + opcode_name (a_opcode) + ")%N") ws.log ("%Non_event (ws, a_message, " + opcode_name (a_opcode) + ")%N", {HTTPD_LOGGER_CONSTANTS}.debug_level)
end end
if a_message = Void then if a_message = Void then
create {STRING} l_message.make_empty create {STRING} l_message.make_empty
@@ -34,150 +45,93 @@ feature -- Web Socket Interface
end end
if a_opcode = Binary_frame then if a_opcode = Binary_frame then
on_binary (conn, l_message) on_binary (ws, l_message)
elseif a_opcode = Text_frame then elseif a_opcode = Text_frame then
on_text (conn, l_message) on_text (ws, l_message)
elseif a_opcode = Pong_frame then elseif a_opcode = Pong_frame then
on_pong (conn, l_message) on_pong (ws, l_message)
elseif a_opcode = Ping_frame then elseif a_opcode = Ping_frame then
on_ping (conn, l_message) on_ping (ws, l_message)
elseif a_opcode = Connection_close_frame then elseif a_opcode = Connection_close_frame then
on_connection_close (conn, "") on_connection_close (ws, "")
else else
on_unsupported (conn, l_message, a_opcode) on_unsupported (ws, l_message, a_opcode)
end end
end end
on_open (conn: HTTPD_STREAM_SOCKET) feature -- Websocket events
on_open (ws: WEB_SOCKET)
-- Called after handshake, indicates that a complete WebSocket connection has been established. -- Called after handshake, indicates that a complete WebSocket connection has been established.
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
deferred deferred
end end
on_binary (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) on_binary (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
deferred deferred
end end
on_pong (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) on_text (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
deferred
end
on_close (ws: detachable WEB_SOCKET)
-- Called after the WebSocket connection is closed.
deferred
end
feature -- Websocket events: implemented
on_pong (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
require
ws_attached: ws /= Void
ws_valid: ws.is_open_read and then ws.is_open_write
do do
-- log ("Its a pong frame") -- log ("Its a pong frame")
-- at first we ignore pong -- at first we ignore pong
-- FIXME: provide better explanation -- FIXME: provide better explanation
end end
on_ping (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) on_ping (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
do do
send (conn, Pong_frame, a_message) ws.send (Pong_frame, a_message)
end end
on_text (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) on_unsupported (ws: WEB_SOCKET; a_message: READABLE_STRING_8; a_opcode: INTEGER)
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
deferred
end
on_unsupported (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8; a_opcode: INTEGER)
require
conn_attached: conn /= Void
conn_valid: conn.is_open_read and then conn.is_open_write
do do
-- do nothing -- do nothing
end end
on_connection_close (conn: HTTPD_STREAM_SOCKET; a_message: detachable READABLE_STRING_8) on_connection_close (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8)
require require
conn_attached: conn /= Void ws_attached: ws /= Void
conn_valid: conn.is_open_read and then conn.is_open_write ws_valid: ws.is_open_read and then ws.is_open_write
do do
send (conn, Connection_close_frame, "") ws.send (Connection_close_frame, "")
end
on_close (conn: detachable HTTPD_STREAM_SOCKET)
-- Called after the WebSocket connection is closed.
deferred
end
feature {NONE} -- Implementation
send (conn: HTTPD_STREAM_SOCKET; a_opcode:INTEGER; a_message: READABLE_STRING_8)
local
i: INTEGER
l_chunk_size: INTEGER
l_chunk: READABLE_STRING_8
l_header_message: STRING
l_message_count: INTEGER
n: NATURAL_64
retried: BOOLEAN
do
print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N")
if not retried then
create l_header_message.make_empty
l_header_message.append_code ((0x80 | a_opcode).to_natural_32)
l_message_count := a_message.count
n := l_message_count.to_natural_64
if l_message_count > 0xffff then
--! Improve. this code needs to be checked.
l_header_message.append_code ((0 | 127).to_natural_32)
l_header_message.append_character ((n |>> 56).to_character_8)
l_header_message.append_character ((n |>> 48).to_character_8)
l_header_message.append_character ((n |>> 40).to_character_8)
l_header_message.append_character ((n |>> 32).to_character_8)
l_header_message.append_character ((n |>> 24).to_character_8)
l_header_message.append_character ((n |>> 16).to_character_8)
l_header_message.append_character ((n |>> 8).to_character_8)
l_header_message.append_character ( n.to_character_8)
elseif l_message_count > 125 then
l_header_message.append_code ((0 | 126).to_natural_32)
l_header_message.append_code ((n |>> 8).as_natural_32)
l_header_message.append_character (n.to_character_8)
else
l_header_message.append_code (n.as_natural_32)
end
conn.put_string (l_header_message)
l_chunk_size := 16_384 -- 16K
if l_message_count < l_chunk_size then
conn.put_string (a_message)
else
from
i := 0
until
l_chunk_size = 0
loop
debug ("ws")
print ("Sending chunk " + (i + 1).out + " -> " + (i + l_chunk_size).out +" / " + l_message_count.out + "%N")
end
l_chunk := a_message.substring (i + 1, l_message_count.min (i + l_chunk_size))
conn.put_string (l_chunk)
if l_chunk.count < l_chunk_size then
l_chunk_size := 0
end
i := i + l_chunk_size
end
debug ("ws")
print ("Sending chunk done%N")
end
end
else
-- FIXME: what should be done on rescue?
end
rescue
retried := True
io.put_string ("Internal error in " + generator + ".do_send (conn, a_opcode=" + a_opcode.out + ", a_message) !%N")
retry
end end
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 end

View File

@@ -1,6 +1,8 @@
note note
description: "Summary description for {WEB_SOCKET}." description: "[
author: "" Object representing the websocket connection.
It contains the `request` and `response`, and more important the `socket` itself.
]"
date: "$Date$" date: "$Date$"
revision: "$Revision$" revision: "$Revision$"
@@ -8,7 +10,11 @@ class
WEB_SOCKET WEB_SOCKET
inherit inherit
WGI_STANDALONE_CONNECTOR_ACCESS WGI_STANDALONE_CONNECTOR_EXPORTER
WSF_RESPONSE_EXPORTER
WGI_EXPORTER
HTTPD_LOGGER_CONSTANTS HTTPD_LOGGER_CONSTANTS
@@ -25,7 +31,8 @@ feature {NONE} -- Initialization
do do
request := req request := req
response := res response := res
is_verbose := True is_verbose := False
verbose_level := notice_level
if if
attached {WGI_STANDALONE_INPUT_STREAM} req.input as r_input attached {WGI_STANDALONE_INPUT_STREAM} req.input as r_input
@@ -40,30 +47,85 @@ feature {NONE} -- Initialization
feature -- Access feature -- Access
socket: HTTPD_STREAM_SOCKET socket: HTTPD_STREAM_SOCKET
-- Underlying connected socket.
feature {NONE} -- Access
request: WSF_REQUEST request: WSF_REQUEST
-- Associated request.
response: WSF_RESPONSE response: WSF_RESPONSE
-- Associated response stream.
feature -- Access feature -- Access
is_websocket: BOOLEAN is_websocket: BOOLEAN
-- Does `open_ws_handshake' detect valid websocket upgrade handshake?
has_error: BOOLEAN feature -- Settings
is_verbose: BOOLEAN is_verbose: BOOLEAN
-- Output verbose log messages?
socket_is_ready_for_reading: BOOLEAN verbose_level: INTEGER
-- Level of verbosity.
feature -- Status
has_error: BOOLEAN
-- Error occured during processing?
feature -- Socket status
is_ready_for_reading: BOOLEAN
-- Is `socket' ready for reading?
--| at this point, socket should be set to blocking.
do do
Result := socket.ready_for_reading Result := socket.ready_for_reading
end end
is_open_read: BOOLEAN
-- Is `socket' open for reading?
do
Result := socket.is_open_read
end
is_open_write: BOOLEAN
-- Is `socket' open for writing?
do
Result := socket.is_open_write
end
socket_descriptor: INTEGER
-- Descriptor for current `socket'.
do
Result := socket.descriptor
end
feature -- Element change feature -- Element change
set_is_verbose (b: BOOLEAN)
do
is_verbose := b
end
set_verbose_level (lev: INTEGER)
do
verbose_level := lev
end
feature -- Basic operation
put_error (a_message: READABLE_STRING_8)
do
response.put_error (a_message)
end
log (m: READABLE_STRING_8; lev: INTEGER) log (m: READABLE_STRING_8; lev: INTEGER)
-- Log `m' in the error channel, i.e stderr for standalone.
do do
if is_verbose then if is_verbose then
response.put_error (m) put_error (m)
end end
end end
@@ -87,15 +149,17 @@ feature -- Basic Operation
local local
l_sha1: SHA1 l_sha1: SHA1
l_key : STRING l_key : STRING
l_handshake: STRING
req: like request req: like request
res: like response res: like response
do do
req := request -- Reset values.
res := response
is_websocket := False is_websocket := False
has_error := False has_error := False
-- Local cache.
req := request
res := response
-- Reading client's opening GT -- Reading client's opening GT
-- TODO extract to a validator handshake or something like that. -- TODO extract to a validator handshake or something like that.
@@ -111,12 +175,6 @@ feature -- Basic Operation
l_upgrade_key.is_case_insensitive_equal_general ("websocket") -- Upgrade header must be present with value websocket l_upgrade_key.is_case_insensitive_equal_general ("websocket") -- Upgrade header must be present with value websocket
then then
is_websocket := True is_websocket := True
-- if
-- attached {WGI_STANDALONE_INPUT_STREAM} req.input as r_input and then
-- attached r_input.source as l_socket
-- then
-- l_socket.set_blocking
-- end
socket.set_blocking socket.set_blocking
if if
attached req.meta_string_variable ("HTTP_SEC_WEBSOCKET_KEY") as l_ws_key and then -- Sec-websocket-key must be present attached req.meta_string_variable ("HTTP_SEC_WEBSOCKET_KEY") as l_ws_key and then -- Sec-websocket-key must be present
@@ -133,23 +191,18 @@ feature -- Basic Operation
create l_sha1.make create l_sha1.make
l_sha1.update_from_string (l_ws_key + magic_guid) l_sha1.update_from_string (l_ws_key + magic_guid)
l_key := Base64_encoder.encoded_string (digest (l_sha1)) l_key := Base64_encoder.encoded_string (digest (l_sha1))
-- create l_handshake.make_from_string ("") --HTTP/1.1 101 Switching Protocols%R%N") res.header.add_header_key_value ("Upgrade", "websocket")
create l_handshake.make_from_string ("HTTP/1.1 101 Switching Protocols%R%N") res.header.add_header_key_value ("Connection", "Upgrade")
l_handshake.append_string ("Upgrade: websocket%R%N") res.header.add_header_key_value ("Sec-WebSocket-Accept", l_key)
l_handshake.append_string ("Connection: Upgrade%R%N")
l_handshake.append_string ("Sec-WebSocket-Accept: ")
l_handshake.append_string (l_key)
l_handshake.append_string ("%R%N")
-- end of header empty line
--not with WSF_RESPONSE l_handshake.append_string ("%R%N")
l_handshake.append_string ("%R%N")
if is_verbose then if is_verbose then
log ("%N================> Send", debug_level) log ("%N================> Send Handshake", debug_level)
log (l_handshake, debug_level) if attached {HTTP_HEADER} res.header as h then
log (h.string, debug_level)
end end
socket.put_string (l_handshake) end
-- res.set_status_code_with_reason_phrase (101, "Switching Protocols") res.set_status_code_with_reason_phrase (101, "Switching Protocols")
-- res.put_header_text (l_handshake) res.wgi_response.push
else else
has_error := True has_error := True
if is_verbose then if is_verbose then
@@ -158,7 +211,6 @@ feature -- Basic Operation
-- If we cannot complete the handshake, then the server MUST stop processing the client's handshake and return an HTTP response with an -- If we cannot complete the handshake, then the server MUST stop processing the client's handshake and return an HTTP response with an
-- appropriate error code (such as 400 Bad Request). -- appropriate error code (such as 400 Bad Request).
res.set_status_code_with_reason_phrase (400, "Bad Request") res.set_status_code_with_reason_phrase (400, "Bad Request")
-- a_socket.put_string ("HTTP/1.1 400 Bad Request%N")
end end
else else
is_websocket := False is_websocket := False
@@ -177,7 +229,9 @@ feature -- Response!
n: NATURAL_64 n: NATURAL_64
retried: BOOLEAN retried: BOOLEAN
do do
debug ("ws")
print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N") print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N")
end
if not retried then if not retried then
create l_header_message.make_empty create l_header_message.make_empty
l_header_message.append_code ((0x80 | a_opcode).to_natural_32) l_header_message.append_code ((0x80 | a_opcode).to_natural_32)
@@ -203,7 +257,7 @@ feature -- Response!
end end
socket.put_string (l_header_message) socket.put_string (l_header_message)
l_chunk_size := 16_384 -- 16K l_chunk_size := 16_384 -- 16K TODO: see if we should make it customizable.
if l_message_count < l_chunk_size then if l_message_count < l_chunk_size then
socket.put_string (a_message) socket.put_string (a_message)
else else
@@ -289,7 +343,6 @@ feature -- Response!
retried: BOOLEAN retried: BOOLEAN
do do
if not retried then if not retried then
-- l_input := request.input
l_socket := socket l_socket := socket
debug ("ws") debug ("ws")
print ("next_frame:%N") print ("next_frame:%N")
@@ -467,9 +520,7 @@ feature -- Response!
if l_remaining_len < l_chunk_size then if l_remaining_len < l_chunk_size then
l_chunk_size := l_remaining_len l_chunk_size := l_remaining_len
end end
-- l_input.read_string (l_chunk_size)
l_socket.read_stream (l_chunk_size) l_socket.read_stream (l_chunk_size)
-- l_bytes_read := l_input.last_string.count
l_bytes_read := l_socket.bytes_read l_bytes_read := l_socket.bytes_read
debug ("ws") 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") 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")

View File

@@ -0,0 +1,152 @@
note
description: "[
To implement websocket handling, provide a `callbacks` object implementing the {WEB_SOCKET_EVENT_I} interface.
]"
date: "$Date$"
revision: "$Revision$"
class
WEB_SOCKET_HANDLER
inherit
WEB_SOCKET_CONSTANTS
REFACTORING_HELPER
HTTPD_LOGGER_CONSTANTS
create
make
feature {NONE} -- Initialization
make (ws: WEB_SOCKET; a_callbacks: WEB_SOCKET_EVENT_I)
do
web_socket := ws
callbacks := a_callbacks
end
feature -- Access
web_socket: WEB_SOCKET
-- Associated websocket.
callbacks: WEB_SOCKET_EVENT_I
feature -- Execution
frozen execute
do
callbacks.on_open (web_socket)
execute_websocket
end
execute_websocket
local
exit: BOOLEAN
l_frame: detachable WEB_SOCKET_FRAME
l_client_message: detachable READABLE_STRING_8
l_utf: UTF_CONVERTER
ws: like web_socket
s: STRING
do
from
-- loop until ws is closed or has error.
ws := web_socket
until
exit
loop
debug ("dbglog")
dbglog (generator + ".execute_websocket (loop) WS_REQUEST_HANDLER.process_request {" + ws.socket_descriptor.out + "}")
end
if ws.is_ready_for_reading then
l_frame := ws.next_frame
if l_frame /= Void and then l_frame.is_valid then
if attached l_frame.injected_control_frames as l_injections then
-- Process injected control frames now.
-- FIXME
across
l_injections as ic
loop
if ic.item.is_connection_close then
-- FIXME: we should probably send this event .. after the `l_frame.parent' frame event.
callbacks.on_event (ws, ic.item.payload_data, ic.item.opcode)
exit := True
elseif ic.item.is_ping then
-- FIXME reply only to the most recent ping ...
callbacks.on_event (ws, ic.item.payload_data, ic.item.opcode)
else
callbacks.on_event (ws, ic.item.payload_data, ic.item.opcode)
end
end
end
l_client_message := l_frame.payload_data
if l_client_message = Void then
l_client_message := ""
end
debug ("ws")
create s.make_from_string ("%NExecute: %N")
s.append (" [opcode: "+ opcode_name (l_frame.opcode) +"]%N")
if l_frame.is_text then
s.append (" [client message: %""+ l_client_message +"%"]%N")
elseif l_frame.is_binary then
s.append (" [client binary message length: %""+ l_client_message.count.out +"%"]%N")
end
s.append (" [is_control: " + l_frame.is_control.out + "]%N")
s.append (" [is_binary: " + l_frame.is_binary.out + "]%N")
s.append (" [is_text: " + l_frame.is_text.out + "]%N")
dbglog (s)
end
if l_frame.is_connection_close then
callbacks.on_event (ws, l_client_message, l_frame.opcode)
exit := True
elseif l_frame.is_binary then
callbacks.on_event (ws, l_client_message, l_frame.opcode)
elseif l_frame.is_text then
check is_valid_utf_8: l_utf.is_valid_utf_8_string_8 (l_client_message) end
callbacks.on_event (ws, l_client_message, l_frame.opcode)
else
callbacks.on_event (ws, l_client_message, l_frame.opcode)
end
else
debug ("ws")
create s.make_from_string ("%NExecute: %N")
s.append (" [ERROR: invalid frame]%N")
if l_frame /= Void and then attached l_frame.error as err then
s.append (" [Code: "+ err.code.out +"]%N")
s.append (" [Description: "+ err.description +"]%N")
end
dbglog (s)
end
callbacks.on_event (ws, "", connection_close_frame)
exit := True -- FIXME: check proper close protocol
end
else
debug ("ws")
dbglog (generator + ".WAITING WS_REQUEST_HANDLER.process_request {" + ws.socket_descriptor.out + "}")
end
end
end
end
feature {NONE} -- Logging
dbglog (m: READABLE_STRING_8)
do
web_socket.log (m, debug_level)
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

@@ -1,6 +1,9 @@
note note
description: "[ description: "[
Objects that ... Request execution based on attributes `request' and `response'.
Also support Upgrade to Websocket protocol.
]" ]"
author: "$Author$" author: "$Author$"
date: "$Date$" date: "$Date$"
@@ -15,138 +18,32 @@ inherit
execute as http_execute execute as http_execute
end end
WEB_SOCKET_CONSTANTS
REFACTORING_HELPER
HTTPD_LOGGER_CONSTANTS
WGI_STANDALONE_CONNECTOR_ACCESS
--create --create
-- make -- make
feature -- Execution feature -- Execution
is_verbose: BOOLEAN frozen http_execute
is_websocket: BOOLEAN
has_error: BOOLEAN
log (m: READABLE_STRING_8; lev: INTEGER)
do
end
http_execute
local local
ws: WEB_SOCKET ws: WEB_SOCKET
ws_h: like new_websocket_handler
do do
has_error := False
is_websocket := False
create ws.make (request, response) create ws.make (request, response)
ws.open_ws_handshake ws.open_ws_handshake
if ws.is_websocket then if ws.is_websocket then
has_error := ws.has_error if ws.has_error then
is_websocket := True -- Upgrade to websocket raised an error
on_open (ws) -- stay on standard HTTP/1.1 protocol
execute_websocket (ws) execute
else
ws_h := new_websocket_handler (ws)
ws_h.execute
end
else else
execute execute
end end
end end
execute_websocket (ws: WEB_SOCKET)
require
is_websocket: is_websocket
local
exit: BOOLEAN
l_frame: detachable WEB_SOCKET_FRAME
l_client_message: detachable READABLE_STRING_8
l_utf: UTF_CONVERTER
do
from
-- loop until ws is closed or has error.
until
has_error or else exit
loop
-- debug ("dbglog")
-- dbglog (generator + ".LOOP WS_REQUEST_HANDLER.process_request {...}")
-- end
if ws.socket_is_ready_for_reading then
l_frame := ws.next_frame
if l_frame /= Void and then l_frame.is_valid then
if attached l_frame.injected_control_frames as l_injections then
-- Process injected control frames now.
-- FIXME
across
l_injections as ic
loop
if ic.item.is_connection_close then
-- FIXME: we should probably send this event .. after the `l_frame.parent' frame event.
on_event (ws, ic.item.payload_data, ic.item.opcode)
exit := True
elseif ic.item.is_ping then
-- FIXME reply only to the most recent ping ...
on_event (ws, ic.item.payload_data, ic.item.opcode)
else
on_event (ws, ic.item.payload_data, ic.item.opcode)
end
end
end
l_client_message := l_frame.payload_data
if l_client_message = Void then
l_client_message := ""
end
-- debug ("ws")
if is_verbose then
print("%NExecute: %N")
print (" [opcode: "+ opcode_name (l_frame.opcode) +"]%N")
if l_frame.is_text then
print (" [client message: %""+ l_client_message +"%"]%N")
elseif l_frame.is_binary then
print (" [client binary message length: %""+ l_client_message.count.out +"%"]%N")
end
print (" [is_control: " + l_frame.is_control.out + "]%N")
print (" [is_binary: " + l_frame.is_binary.out + "]%N")
print (" [is_text: " + l_frame.is_text.out + "]%N")
end
if l_frame.is_connection_close then
on_event (ws, l_client_message, l_frame.opcode)
exit := True
elseif l_frame.is_binary then
on_event (ws, l_client_message, l_frame.opcode)
elseif l_frame.is_text then
check is_valid_utf_8: l_utf.is_valid_utf_8_string_8 (l_client_message) end
on_event (ws, l_client_message, l_frame.opcode)
else
on_event (ws, l_client_message, l_frame.opcode)
end
else
-- debug ("ws")
if is_verbose then
print("%NExecute: %N")
print (" [ERROR: invalid frame]%N")
if l_frame /= Void and then attached l_frame.error as err then
print (" [Code: "+ err.code.out +"]%N")
print (" [Description: "+ err.description +"]%N")
end
end
on_event (ws, "", connection_close_frame)
exit := True
end
else
if is_verbose then
log (generator + ".WAITING WS_REQUEST_HANDLER.process_request {..}", debug_level)
end
end
end
end
execute execute
-- Execute Current request, -- Execute Current request,
-- getting data from `request' -- getting data from `request'
@@ -154,79 +51,21 @@ feature -- Execution
deferred deferred
end end
feature -- Web Socket Interface feature -- Factory
on_event (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8; a_opcode: INTEGER) new_websocket_handler (ws: WEB_SOCKET): WEB_SOCKET_HANDLER
-- Called when a frame from the client has been receive -- Websocket request specific handler on socket `ws'.
local --| For the creation, it requires an instance of `{WEB_SOCKET_EVENT_I}'
l_message: READABLE_STRING_8 --| to receive the websocket events.
do --| One can inherit from {WEB_SOCKET_EVENT_I} and implement the related
debug ("ws") --| deferred features.
print ("%Non_event (conn, a_message, " + opcode_name (a_opcode) + ")%N") --| Or even provide a new class implementing {WEB_SOCKET_EVENT_I}.
end require
if a_message = Void then is_websocket: ws.is_websocket
create {STRING} l_message.make_empty no_error: not ws.has_error
else
l_message := a_message
end
if a_opcode = Binary_frame then
on_binary (ws, l_message)
elseif a_opcode = Text_frame then
on_text (ws, l_message)
elseif a_opcode = Pong_frame then
on_pong (ws, l_message)
elseif a_opcode = Ping_frame then
on_ping (ws, l_message)
elseif a_opcode = Connection_close_frame then
on_connection_close (ws, "")
else
on_unsupported (ws, l_message, a_opcode)
end
end
on_open (ws: WEB_SOCKET)
-- Called after handshake, indicates that a complete WebSocket connection has been established.
deferred deferred
end end
on_binary (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
deferred
end
on_pong (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
do
-- log ("Its a pong frame")
-- at first we ignore pong
-- FIXME: provide better explanation
end
on_ping (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
do
ws.send (Pong_frame, a_message)
end
on_text (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
deferred
end
on_unsupported (ws: WEB_SOCKET; a_message: READABLE_STRING_8; a_opcode: INTEGER)
do
-- do nothing
fixme ("implement on_unsupported")
end
on_connection_close (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8)
do
ws.send (Connection_close_frame, "")
end
on_close (ws: WEB_SOCKET)
-- Called after the WebSocket connection is closed.
deferred
end
note note
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"