note description: "HTTPD handler interface processing request." date: "$Date$" revision: "$Revision$" deferred class HTTPD_REQUEST_HANDLER_I inherit HTTPD_DEBUG_FACILITIES HTTPD_LOGGER_CONSTANTS HTTPD_SOCKET_FACTORY feature {NONE} -- Initialization make (a_request_settings: HTTPD_REQUEST_SETTINGS) do reset -- Import global request settings. timeout_ns := a_request_settings.timeout_ns -- nanoseconds socket_recv_timeout_ns := a_request_settings.socket_recv_timeout_ns -- nanoseconds keep_alive_timeout_ns := a_request_settings.keep_alive_timeout_ns -- nanoseconds max_keep_alive_requests := a_request_settings.max_keep_alive_requests is_verbose := a_request_settings.is_verbose verbose_level := a_request_settings.verbose_level is_secure := a_request_settings.is_secure end reset do reset_request (False) reset_error if attached internal_client_socket as l_sock then l_sock.cleanup end internal_client_socket := Void end reset_request (a_is_reusing_connection: BOOLEAN) -- Reset the request, and `a_is_reusing_connection' says if the peristent connection is -- still alive. do if a_is_reusing_connection then -- Keep `remote_info' as it stays the same for the successive -- persistent connections. else remote_info := Void end version := Void -- FIXME: optimize to just wipe_out if needed create method.make_empty create uri.make_empty create request_header.make_empty create request_header_map.make (10) is_persistent_connection_requested := False end feature -- Status report is_connected: BOOLEAN -- Is handler connected to incoming request via `client_socket'? do Result := client_socket.descriptor_available end feature -- Access internal_client_socket: detachable HTTPD_STREAM_SOCKET client_socket: HTTPD_STREAM_SOCKET local s: like internal_client_socket do s := internal_client_socket if s = Void then s := new_client_socket (is_secure) internal_client_socket := s end Result := s end request_header: STRING -- Header' source request_header_map: HASH_TABLE [STRING, STRING] -- Contains key:value of the header method: STRING -- http verb uri: STRING -- http endpoint version: detachable STRING -- http_version --| unused for now remote_info: detachable TUPLE [addr: STRING; hostname: STRING; port: INTEGER] -- Information related to remote client is_persistent_connection_requested: BOOLEAN -- Persistent connection requested? -- either has "Connection: keep-alive" header, -- or is HTTP/1.1 and no header "Connection: close". is_http_version_1_0: BOOLEAN do Result := not attached version as v or else v.same_string ("HTTP/1.0") end is_http_version_1_1: BOOLEAN do Result := not attached version as v or else v.same_string ("HTTP/1.1") end is_http_version_2: BOOLEAN do Result := not attached version as v or else v.same_string ("HTTP/2.0") end feature -- Settings is_verbose: BOOLEAN -- Output messages? verbose_level: INTEGER -- Output verbosity. is_secure: BOOLEAN -- Is secure socket? -- i.e: SSL? is_persistent_connection_supported: BOOLEAN -- Is persistent connection supported? do Result := {HTTPD_SERVER}.is_persistent_connection_supported and then max_keep_alive_requests /= 0 --| `-1` no limit end is_next_persistent_connection_supported: BOOLEAN -- Is next persistent connection supported? -- note: it is relevant only if `is_persistent_connection_supported' is True. timeout_ns: NATURAL_64 -- nanoseconds -- Amount of nanoseconds that the server waits for receipts and transmissions during communications. socket_recv_timeout_ns: NATURAL_64 -- nanoseconds -- Amount of nanoseconds that the server waits for receiving data on socket during communications. max_keep_alive_requests: INTEGER -- Maximum number of requests allowed per persistent connection. keep_alive_timeout_ns: NATURAL_64 -- nanoseconds -- Number of nanoseconds for persistent connection timeout. feature -- Status report has_error: BOOLEAN -- Error occurred during `get_request_header' feature -- Status change report_error (m: detachable READABLE_STRING_GENERAL) -- Report error occurred, with optional message `m'. do has_error := True if m /= Void and then is_verbose then log ({UTF_CONVERTER}.utf_32_string_to_utf_8_string_8 (m), debug_level) end end reset_error -- Reset previous error for current request handler. do has_error := False end feature -- Change set_is_verbose (b: BOOLEAN) -- Set `is_verbose' with `b'. do is_verbose := b ensure is_verbose_set: is_verbose = b end feature -- Execution safe_execute -- Execute accepted incoming connection as request. local retried: BOOLEAN do if retried then release else if not has_error and then is_connected then execute end release end rescue retried := True retry end execute require is_connected: is_connected local l_socket: like client_socket l_remote_info: detachable like remote_info l_exit: BOOLEAN n,m: INTEGER do l_socket := client_socket -- Set to expected `timeout_ns`. l_socket.set_timeout_ns (timeout_ns) l_socket.set_recv_timeout_ns (socket_recv_timeout_ns) -- Compute remote info once for the persistent connection. create l_remote_info if attached l_socket.peer_address as l_addr then l_remote_info.addr := l_addr.host_address.host_address l_remote_info.hostname := {UTF_CONVERTER}.utf_32_string_to_utf_8_string_8 (l_addr.host_address.host_name) -- FIXME jfiat [2022/01/20] : check what should be done for hostname... l_remote_info.port := l_addr.port end remote_info := l_remote_info check socket_attached: l_socket /= Void socket_valid: l_socket.is_open_read and then l_socket.is_open_write end from -- Process persistent connection as long the socket is not closed. n := 0 m := max_keep_alive_requests is_next_persistent_connection_supported := True until l_exit loop n := n + 1 -- If `m` is `-1`, no limit for the number of keep_alive requests. if m >= 0 and n >= m then is_next_persistent_connection_supported := False elseif n > 1 and is_verbose then log ("Reuse connection (" + n.out + ")", information_level) end -- FIXME: it seems to be called one more time, mostly to see this is done. execute_request (n > 1) l_exit := not is_persistent_connection_supported or not is_next_persistent_connection_supported -- related to `max_keep_alive_requests' or not is_persistent_connection_requested or has_error or l_socket.is_closed or not l_socket.is_open_read reset_request (not l_exit) end if l_exit and has_error and not l_socket.is_closed then l_socket.close end end execute_request (a_is_reusing_connection: BOOLEAN) -- Execute http request, and if `a_is_reusing_connection' is True -- the execution is reusing the persistent connection. require is_connected: is_connected reuse_connection_when_possible: a_is_reusing_connection implies is_persistent_connection_supported no_error: not has_error remote_info_set: remote_info /= Void local l_socket: like client_socket do debug ("dbglog") if a_is_reusing_connection then dbglog ("execute_request: wait on persistent connection.") end end reset_error l_socket := client_socket check socket_attached: l_socket /= Void socket_valid: l_socket.is_open_read and then l_socket.is_open_write end if l_socket.is_closed then debug ("dbglog") dbglog ("execute_request {socket is Closed!}") end else debug ("dbglog") dbglog ("execute_request socket=" + l_socket.descriptor.out + " ENTER") end -- Try to get request header. -- If the request is reusing persistent connection, use `keep_alive_timeout_ns', -- otherwise `socket_recv_timeout_ns'. get_request_header (l_socket, a_is_reusing_connection) if has_error then if a_is_reusing_connection and then request_header.is_empty then -- Close persistent connection, since no new connection occurred in the delay `keep_alive_timeout_ns'. debug ("dbglog") dbglog ("execute_request socket=" + l_socket.descriptor.out + "} close persistent connection.") end else if is_verbose then log (request_header + "%NWARNING: invalid HTTP incoming request", warning_level) end process_bad_request (l_socket) end is_persistent_connection_requested := False else if is_verbose then log_with_separation_line (request_header, information_level) end process_request (l_socket) end debug ("dbglog") dbglog ("execute_request {" + l_socket.descriptor.out + "} LEAVE") end end end release do reset end feature -- Request processing process_request (a_socket: HTTPD_STREAM_SOCKET) -- Process request on socket `a_socket'. require no_error: not has_error a_uri_attached: uri /= Void a_method_attached: method /= Void a_header_map_attached: request_header_map /= Void a_header_text_attached: request_header /= Void a_socket_attached: a_socket /= Void deferred end process_bad_request (a_socket: HTTPD_STREAM_SOCKET) -- Process bad request catched on `a_socket'. require has_error: has_error a_socket_attached: a_socket /= Void local -- h: STRING -- s: STRING do -- NOTE: this is experiment code, and not ready yet. -- if a_socket.ready_for_writing then -- s := "{ -- -- --400 Bad Request -- --

Bad Request

-- -- }" -- create h.make (1_024) -- h.append ("HTTP/1.1 400 Bad Request%R%N") -- h.append ("Content-Length: " + s.count.out + "%R%N") -- h.append ("Connection: close%R%N") -- h.append ("Content-Type: text/html; charset=iso-8859-1%R%N") -- h.append ("%R%N") -- a_socket.put_string (h) -- if a_socket.ready_for_writing then -- a_socket.put_string (s) -- end -- end end feature -- Parsing get_request_header (a_socket: HTTPD_STREAM_SOCKET; a_is_reusing_connection: BOOLEAN) -- Analyze message extracted from `a_socket' as HTTP request. -- If `a_is_reusing_connection' is True, then first use -- Note: it reads from socket. -- Note: it updates `request_header' and `request_header_map', and eventually `is_persistent_connection_requested'. require input_readable: a_socket /= Void and then a_socket.is_open_read local end_of_stream: BOOLEAN pos, n: INTEGER line: detachable STRING k, val: STRING txt: STRING l_is_verbose: BOOLEAN do create txt.make (64) request_header := txt l_is_verbose := is_verbose if not has_error and then a_socket.readable then if a_is_reusing_connection then a_socket.set_recv_timeout_ns (keep_alive_timeout_ns) -- in nanoseconds! else a_socket.set_recv_timeout_ns (socket_recv_timeout_ns) -- FIXME: return a 408 Request Timeout response .. end if attached next_line (a_socket) as l_request_line and then not l_request_line.is_empty and then not has_error then txt.append (l_request_line) txt.append_character ('%N') analyze_request_line (l_request_line) if not has_error then if a_is_reusing_connection then -- Restore normal recv timeout! a_socket.set_recv_timeout_ns (socket_recv_timeout_ns) -- FIXME: return a 408 Request Timeout response .. end from line := next_line (a_socket) until line = Void or end_of_stream or has_error loop n := line.count debug ("ew_standalone") if l_is_verbose then log (line, debug_level) end end pos := line.index_of (':', 1) if pos > 0 then k := line.substring (1, pos - 1) if line [pos + 1].is_space then pos := pos + 1 end if line [n] = '%R' then n := n - 1 end val := line.substring (pos + 1, n) request_header_map.put (val, k) end txt.append (line) txt.append_character ('%N') if line.is_empty or else line [1] = '%R' then end_of_stream := True else line := next_line (a_socket) end end -- Except for HTTP/1.0, persistent connection is the default. is_persistent_connection_requested := True if is_http_version_1_0 then is_persistent_connection_requested := has_keep_alive_http_connection_header (request_header_map) else -- By default HTTP:1/1 support persistent connection. if has_close_http_connection_header (request_header_map) then is_persistent_connection_requested := False else is_persistent_connection_requested := True end end end else report_error ("Bad header line (empty)") end else report_error ("Socket is not readable") end end has_keep_alive_http_connection_header (h_map: like request_header_map): BOOLEAN -- Does Current request header map `h_map' have "keep-alive" connection header? local i: INTEGER do if attached h_map.item ("Connection") as l_connection then -- Could be for instance "keep-alive, Upgrade" i := l_connection.substring_index ("keep-alive", 1) if i > 0 then i := i + 9 -- "keep-alive" has 10 characters check i <= l_connection.count end if i = l_connection.count then Result := True else Result := l_connection [i + 1] = ',' or l_connection [i + 1].is_space end end end end has_close_http_connection_header (h_map: like request_header_map): BOOLEAN -- Does Current request header map `h_map' have "close" connection header? local i: INTEGER do if attached h_map.item ("Connection") as l_connection then -- Could be for instance "close, ..." i := l_connection.substring_index ("close", 1) if i > 0 then i := i + 4 -- "close" has 5 characters check i <= l_connection.count end if i = l_connection.count then Result := True else Result := l_connection [i + 1] = ',' or l_connection [i + 1].is_space end end end end analyze_request_line (line: STRING) -- Analyze `line' as a HTTP request line. -- note: may update `has_error'. require valid_line: line /= Void and then not line.is_empty local n, pos, next_pos: INTEGER do debug ("ew_standalone") if is_verbose then log ("%N## Parse HTTP request line ##", debug_level) log (line, debug_level) end end pos := line.index_of (' ', 1) method := line.substring (1, pos - 1) next_pos := line.index_of (' ', pos + 1) uri := line.substring (pos + 1, next_pos - 1) n := line.count if line[n] = '%R' then n := n - 1 end version := line.substring (next_pos + 1, n) if method.is_empty then report_error ("Missing request method data") end end next_line (a_socket: HTTPD_STREAM_SOCKET): detachable STRING -- Next line fetched from `a_socket' is available. -- note: may update `has_error'. require not_has_error: not has_error is_readable: a_socket.is_open_read local retried: BOOLEAN do if retried then report_error ("Rescue in next_line") a_socket.close Result := Void elseif a_socket.readable then a_socket.read_line_noexception Result := a_socket.last_string -- Do no check `was_error' before socket operation, -- otherwise it may be False, due to error during other socket operation in same thread. if a_socket.was_error then report_error ("Socket error") if is_verbose then log (request_header +"%N" + Result + "%N## Network error: " + a_socket.error + " ##", debug_level) end end else -- Error with socket... report_error ("Socket error: not readable") if is_verbose then log (request_header + "%N## Socket is not readable! ##", debug_level) end end rescue -- In case of network error exception (as EiffelNet reports error raising exception) retried := True retry end feature -- Output logger: detachable HTTPD_LOGGER set_logger (a_logger: like logger) -- Set `logger' with `a_logger'. do logger := a_logger ensure logger_set: logger = a_logger end log_with_separation_line (m: STRING; a_level: INTEGER) -- Log message `m'. require is_verbose: is_verbose local s: STRING do if is_verbose and (verbose_level & a_level) = a_level then create s.make (m.count + 42) s.append (create {STRING}.make_filled ('-', 40)) s.append_character ('%N') s.append (m) if attached logger as l_logger then l_logger.log (s) else io.put_string (s + "%N") end end end log (m: STRING; a_level: INTEGER) -- Log message `m'. require is_verbose: is_verbose do if is_verbose and (verbose_level & a_level) = a_level then if attached logger as l_logger then l_logger.log (m) else io.put_string (m + "%N") end end end feature {NONE} -- Helpers socket_has_incoming_data (a_socket: HTTPD_STREAM_SOCKET): BOOLEAN -- Is there any data to read on `a_socket' ? require a_socket.readable do -- FIXME: check if both are really needed. -- Result := a_socket.ready_for_reading --and then a_socket.has_incoming_data -- Result := a_socket.has_incoming_data Result := True end invariant request_header_attached: request_header /= Void note copyright: "2011-2018, 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