From 193cc3cbde9f5c481244e19f7a40b1783275046e Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Wed, 22 Jun 2016 10:46:15 +0200 Subject: [PATCH] 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. --- examples/websocket/application.e | 29 +++ examples/websocket/application_execution.e | 176 +++++++++++++++ examples/websocket/websocket_app.ecf | 21 ++ examples/websocket/ws.ini | 8 + ....e => wgi_standalone_connector_exporter.e} | 2 +- .../src/wgi_standalone_input_stream.e | 2 +- .../wsf/connector/standalone_websocket.ecf | 25 +++ .../websocket/event/web_socket_event_i.e | 184 ++++++--------- .../websocket/web_socket.e | 125 ++++++++--- .../websocket/web_socket_handler.e | 152 +++++++++++++ .../wsf_websocket_execution.e | 211 +++--------------- 11 files changed, 595 insertions(+), 340 deletions(-) create mode 100644 examples/websocket/application.e create mode 100644 examples/websocket/application_execution.e create mode 100644 examples/websocket/websocket_app.ecf create mode 100644 examples/websocket/ws.ini rename library/server/ewsgi/connectors/standalone/src/{wgi_standalone_connector_access.e => wgi_standalone_connector_exporter.e} (82%) create mode 100644 library/server/wsf/connector/standalone_websocket.ecf create mode 100644 library/server/wsf/connector/standalone_websocket/websocket/web_socket_handler.e diff --git a/examples/websocket/application.e b/examples/websocket/application.e new file mode 100644 index 00000000..72f4f627 --- /dev/null +++ b/examples/websocket/application.e @@ -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 diff --git a/examples/websocket/application_execution.e b/examples/websocket/application_execution.e new file mode 100644 index 00000000..5ebd702e --- /dev/null +++ b/examples/websocket/application_execution.e @@ -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 ("

Websocket demo

") + 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 := "[ + + + + + + + +WebSockets Client + + +
+
+

WebSockets Client

+
+ + +
+
+ + + ]" + Result.replace_substring_all ("##PORTNUMBER##", a_port.out) + end + + +end diff --git a/examples/websocket/websocket_app.ecf b/examples/websocket/websocket_app.ecf new file mode 100644 index 00000000..13f95276 --- /dev/null +++ b/examples/websocket/websocket_app.ecf @@ -0,0 +1,21 @@ + + + + + + /.svn$ + /CVS$ + /EIFGENs$ + + + + + + + + + + + diff --git a/examples/websocket/ws.ini b/examples/websocket/ws.ini new file mode 100644 index 00000000..b04bd038 --- /dev/null +++ b/examples/websocket/ws.ini @@ -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 diff --git a/library/server/ewsgi/connectors/standalone/src/wgi_standalone_connector_access.e b/library/server/ewsgi/connectors/standalone/src/wgi_standalone_connector_exporter.e similarity index 82% rename from library/server/ewsgi/connectors/standalone/src/wgi_standalone_connector_access.e rename to library/server/ewsgi/connectors/standalone/src/wgi_standalone_connector_exporter.e index 622cce56..bbfa7ca4 100644 --- a/library/server/ewsgi/connectors/standalone/src/wgi_standalone_connector_access.e +++ b/library/server/ewsgi/connectors/standalone/src/wgi_standalone_connector_exporter.e @@ -6,6 +6,6 @@ note revision: "$Revision$" deferred class - WGI_STANDALONE_CONNECTOR_ACCESS + WGI_STANDALONE_CONNECTOR_EXPORTER end diff --git a/library/server/ewsgi/connectors/standalone/src/wgi_standalone_input_stream.e b/library/server/ewsgi/connectors/standalone/src/wgi_standalone_input_stream.e index 0cdcb382..1cf333a7 100644 --- a/library/server/ewsgi/connectors/standalone/src/wgi_standalone_input_stream.e +++ b/library/server/ewsgi/connectors/standalone/src/wgi_standalone_input_stream.e @@ -24,7 +24,7 @@ feature {NONE} -- Initialization set_source (a_source) 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) do diff --git a/library/server/wsf/connector/standalone_websocket.ecf b/library/server/wsf/connector/standalone_websocket.ecf new file mode 100644 index 00000000..0783e778 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket.ecf @@ -0,0 +1,25 @@ + + + + + + /EIFGENs$ + /\.git$ + /\.svn$ + + + + + + + + + + + + + + + + diff --git a/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e b/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e index b8e8994d..9ad0fec3 100644 --- a/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e +++ b/library/server/wsf/connector/standalone_websocket/websocket/event/web_socket_event_i.e @@ -1,7 +1,18 @@ note description: "[ - API to perform actions like opening and closing the connection, sending and receiving messages, and listening - for events. + Websocket callback events for actions like opening and closing the connection, + 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$" revision: "$Revision$" @@ -16,16 +27,16 @@ inherit 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 require - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.is_open_write + ws_attached: ws /= Void + ws_valid: ws.is_open_read and then ws.is_open_write local l_message: READABLE_STRING_8 do 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 if a_message = Void then create {STRING} l_message.make_empty @@ -34,150 +45,93 @@ feature -- Web Socket Interface end if a_opcode = Binary_frame then - on_binary (conn, l_message) + on_binary (ws, l_message) elseif a_opcode = Text_frame then - on_text (conn, l_message) + on_text (ws, l_message) elseif a_opcode = Pong_frame then - on_pong (conn, l_message) + on_pong (ws, l_message) elseif a_opcode = Ping_frame then - on_ping (conn, l_message) + on_ping (ws, l_message) elseif a_opcode = Connection_close_frame then - on_connection_close (conn, "") + on_connection_close (ws, "") else - on_unsupported (conn, l_message, a_opcode) + on_unsupported (ws, l_message, a_opcode) 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. require - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.is_open_write + ws_attached: ws /= Void + ws_valid: ws.is_open_read and then ws.is_open_write deferred end - on_binary (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + on_binary (ws: WEB_SOCKET; a_message: READABLE_STRING_8) require - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.is_open_write + ws_attached: ws /= Void + ws_valid: ws.is_open_read and then ws.is_open_write deferred end - on_pong (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + on_text (ws: WEB_SOCKET; a_message: READABLE_STRING_8) require - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.is_open_write + ws_attached: ws /= Void + 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 -- log ("Its a pong frame") -- at first we ignore pong -- FIXME: provide better explanation end - on_ping (conn: HTTPD_STREAM_SOCKET; a_message: READABLE_STRING_8) + on_ping (ws: WEB_SOCKET; a_message: READABLE_STRING_8) require - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.is_open_write + ws_attached: ws /= Void + ws_valid: ws.is_open_read and then ws.is_open_write do - send (conn, Pong_frame, a_message) + ws.send (Pong_frame, a_message) 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 - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.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 + ws_attached: ws /= Void + ws_valid: ws.is_open_read and then ws.is_open_write do -- do nothing 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 - conn_attached: conn /= Void - conn_valid: conn.is_open_read and then conn.is_open_write + ws_attached: ws /= Void + ws_valid: ws.is_open_read and then ws.is_open_write do - send (conn, 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 + ws.send (Connection_close_frame, "") 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 diff --git a/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e b/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e index b1c4b8d8..e301f25f 100644 --- a/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e +++ b/library/server/wsf/connector/standalone_websocket/websocket/web_socket.e @@ -1,6 +1,8 @@ note - description: "Summary description for {WEB_SOCKET}." - author: "" + description: "[ + Object representing the websocket connection. + It contains the `request` and `response`, and more important the `socket` itself. + ]" date: "$Date$" revision: "$Revision$" @@ -8,7 +10,11 @@ class WEB_SOCKET inherit - WGI_STANDALONE_CONNECTOR_ACCESS + WGI_STANDALONE_CONNECTOR_EXPORTER + + WSF_RESPONSE_EXPORTER + + WGI_EXPORTER HTTPD_LOGGER_CONSTANTS @@ -25,7 +31,8 @@ feature {NONE} -- Initialization do request := req response := res - is_verbose := True + is_verbose := False + verbose_level := notice_level if attached {WGI_STANDALONE_INPUT_STREAM} req.input as r_input @@ -40,30 +47,85 @@ feature {NONE} -- Initialization feature -- Access socket: HTTPD_STREAM_SOCKET + -- Underlying connected socket. + +feature {NONE} -- Access request: WSF_REQUEST + -- Associated request. response: WSF_RESPONSE + -- Associated response stream. feature -- Access is_websocket: BOOLEAN + -- Does `open_ws_handshake' detect valid websocket upgrade handshake? - has_error: BOOLEAN +feature -- Settings 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 Result := socket.ready_for_reading 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 + 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' in the error channel, i.e stderr for standalone. do if is_verbose then - response.put_error (m) + put_error (m) end end @@ -87,15 +149,17 @@ feature -- Basic Operation local l_sha1: SHA1 l_key : STRING - l_handshake: STRING req: like request res: like response do - req := request - res := response + -- Reset values. is_websocket := False has_error := False + -- Local cache. + req := request + res := response + -- Reading client's opening GT -- 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 then 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 if 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 l_sha1.update_from_string (l_ws_key + magic_guid) l_key := Base64_encoder.encoded_string (digest (l_sha1)) --- create l_handshake.make_from_string ("") --HTTP/1.1 101 Switching Protocols%R%N") - create l_handshake.make_from_string ("HTTP/1.1 101 Switching Protocols%R%N") - l_handshake.append_string ("Upgrade: websocket%R%N") - 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") + res.header.add_header_key_value ("Upgrade", "websocket") + res.header.add_header_key_value ("Connection", "Upgrade") + res.header.add_header_key_value ("Sec-WebSocket-Accept", l_key) + if is_verbose then - log ("%N================> Send", debug_level) - log (l_handshake, debug_level) + log ("%N================> Send Handshake", debug_level) + if attached {HTTP_HEADER} res.header as h then + log (h.string, debug_level) + end end - socket.put_string (l_handshake) --- res.set_status_code_with_reason_phrase (101, "Switching Protocols") --- res.put_header_text (l_handshake) + res.set_status_code_with_reason_phrase (101, "Switching Protocols") + res.wgi_response.push else has_error := True 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 -- appropriate error code (such as 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 else is_websocket := False @@ -177,7 +229,9 @@ feature -- Response! n: NATURAL_64 retried: BOOLEAN do - print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N") + debug ("ws") + print (">>do_send (..., "+ opcode_name (a_opcode) +", ..)%N") + end if not retried then create l_header_message.make_empty l_header_message.append_code ((0x80 | a_opcode).to_natural_32) @@ -203,7 +257,7 @@ feature -- Response! end 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 socket.put_string (a_message) else @@ -289,7 +343,6 @@ feature -- Response! retried: BOOLEAN do if not retried then --- l_input := request.input l_socket := socket debug ("ws") print ("next_frame:%N") @@ -467,9 +520,7 @@ feature -- Response! if l_remaining_len < l_chunk_size then l_chunk_size := l_remaining_len end --- l_input.read_string (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 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") diff --git a/library/server/wsf/connector/standalone_websocket/websocket/web_socket_handler.e b/library/server/wsf/connector/standalone_websocket/websocket/web_socket_handler.e new file mode 100644 index 00000000..5268c600 --- /dev/null +++ b/library/server/wsf/connector/standalone_websocket/websocket/web_socket_handler.e @@ -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 diff --git a/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e b/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e index 0505795d..78be0243 100644 --- a/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e +++ b/library/server/wsf/connector/standalone_websocket/wsf_websocket_execution.e @@ -1,6 +1,9 @@ note description: "[ - Objects that ... + Request execution based on attributes `request' and `response'. + Also support Upgrade to Websocket protocol. + + ]" author: "$Author$" date: "$Date$" @@ -15,138 +18,32 @@ inherit execute as http_execute end - WEB_SOCKET_CONSTANTS - - REFACTORING_HELPER - - HTTPD_LOGGER_CONSTANTS - - WGI_STANDALONE_CONNECTOR_ACCESS - --create -- make feature -- Execution - is_verbose: BOOLEAN - - is_websocket: BOOLEAN - - has_error: BOOLEAN - - log (m: READABLE_STRING_8; lev: INTEGER) - do - - end - - http_execute + frozen http_execute local ws: WEB_SOCKET + ws_h: like new_websocket_handler do - has_error := False - is_websocket := False create ws.make (request, response) ws.open_ws_handshake if ws.is_websocket then - has_error := ws.has_error - is_websocket := True - on_open (ws) - execute_websocket (ws) + if ws.has_error then + -- Upgrade to websocket raised an error + -- stay on standard HTTP/1.1 protocol + execute + else + ws_h := new_websocket_handler (ws) + ws_h.execute + end else execute 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 Current request, -- getting data from `request' @@ -154,79 +51,21 @@ feature -- Execution deferred end -feature -- Web Socket Interface +feature -- Factory - on_event (ws: WEB_SOCKET; a_message: detachable READABLE_STRING_8; a_opcode: INTEGER) - -- Called when a frame from the client has been receive - local - l_message: READABLE_STRING_8 - do - debug ("ws") - print ("%Non_event (conn, a_message, " + opcode_name (a_opcode) + ")%N") - end - if a_message = Void then - create {STRING} l_message.make_empty - 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. + new_websocket_handler (ws: WEB_SOCKET): WEB_SOCKET_HANDLER + -- Websocket request specific handler on socket `ws'. + --| For the creation, it requires an instance of `{WEB_SOCKET_EVENT_I}' + --| to receive the websocket events. + --| One can inherit from {WEB_SOCKET_EVENT_I} and implement the related + --| deferred features. + --| Or even provide a new class implementing {WEB_SOCKET_EVENT_I}. + require + is_websocket: ws.is_websocket + no_error: not ws.has_error deferred 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 copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"