Adopted the nanoseconds timeout precision - in config file added support for ns, us, ms, s timeout precision (without indication, it uses `seconds` precision).
664 lines
18 KiB
Plaintext
664 lines
18 KiB
Plaintext
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 seconds that the server waits for receipts and transmissions during communications.
|
|
|
|
socket_recv_timeout_ns: NATURAL_64 -- nanoseconds
|
|
-- Amount of seconds 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 seconds 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 (m.as_string_8, 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 := l_addr.host_address.host_name
|
|
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',
|
|
-- otherwise `socket_recv_timeout'.
|
|
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'.
|
|
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 := "{
|
|
--<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
|
--<html><head>
|
|
--<title>400 Bad Request</title>
|
|
--</head><body>
|
|
--<h1>Bad Request</h1>
|
|
--</body></html>
|
|
-- }"
|
|
-- 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
|