From 211fc425a3a19f9da4ff154904dcbaeef596c2cb Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Fri, 3 Nov 2017 18:00:39 +0100 Subject: [PATCH] Added handler to add support for CGI scripts. Added a new tool `httpd` which is a basic httpd server product (with file server and CGI handler). --- .../extension/handler/cgi/wsf_cgi_handler.e | 236 ++++++++++++++++++ library/server/wsf/wsf_extension.ecf | 3 +- tools/httpd/README.md | 12 + tools/httpd/httpd.ecf | 56 +++++ tools/httpd/server.ini | 9 + tools/httpd/src/application.e | 31 +++ tools/httpd/src/application_execution.e | 95 +++++++ tools/httpd/www/home.html | 1 + 8 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 library/server/wsf/extension/handler/cgi/wsf_cgi_handler.e create mode 100644 tools/httpd/README.md create mode 100644 tools/httpd/httpd.ecf create mode 100644 tools/httpd/server.ini create mode 100644 tools/httpd/src/application.e create mode 100644 tools/httpd/src/application_execution.e create mode 100644 tools/httpd/www/home.html diff --git a/library/server/wsf/extension/handler/cgi/wsf_cgi_handler.e b/library/server/wsf/extension/handler/cgi/wsf_cgi_handler.e new file mode 100644 index 00000000..2efaf68b --- /dev/null +++ b/library/server/wsf/extension/handler/cgi/wsf_cgi_handler.e @@ -0,0 +1,236 @@ +note + description: "Handler to process CGI script." + date: "$Date$" + revision: "$Revision$" + +class + WSF_CGI_HANDLER + +inherit + WSF_EXECUTE_HANDLER + +create + make + +feature {NONE} -- Creation + + make (a_dir: PATH) + do + working_direction := a_dir + end + +feature -- Settings + + buffer_size: INTEGER = 1_024 + + working_direction: PATH + +feature -- Status report + + cgi_file_path (req: WSF_REQUEST): PATH + local + l_path_info: READABLE_STRING_32 + do + -- Path to CGI executable. + l_path_info := req.path_info + if l_path_info.starts_with_general ("/") then + l_path_info := l_path_info.substring (2, l_path_info.count) + end + + -- Process + Result := working_direction.extended (l_path_info) + end + + exists (req: WSF_REQUEST): BOOLEAN + -- CGI file exists? + do + Result := (create {FILE_UTILITIES}).file_path_exists (cgi_file_path (req)) + end + +feature -- Execution + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + local + fut: FILE_UTILITIES + l_exec_path: PATH + proc: BASE_PROCESS + l_input_env: STRING_TABLE [READABLE_STRING_GENERAL] + l_input_header: detachable STRING + l_input_buf: STRING + l_output: STRING + l_output_header_sent: BOOLEAN + l_error: STRING + s: STRING + i, j, n: INTEGER + do + -- Header + if attached req.raw_header_data as l_header then + l_input_header := l_header + end + -- Input data + create l_input_buf.make (req.content_length_value.to_integer_32) + req.read_input_data_into (l_input_buf) + + -- Input environment + create l_input_env.make (10) + across + req.meta_variables as ic + loop + if attached {WSF_STRING} ic.item as var then + l_input_env.force (var.value, var.name) + else + check supported: False end + end + end + -- No need to import `l_input_header` in environment + -- As current connector already did the job. + + -- Process + l_exec_path := cgi_file_path (req) + if fut.file_path_exists (l_exec_path) then + proc := (create {BASE_PROCESS_FACTORY}).process_launcher (l_exec_path.name, Void, working_direction.name) + proc.set_hidden (True) + proc.set_environment_variable_table (l_input_env) + proc.set_separate_console (False) + proc.redirect_input_to_stream + proc.redirect_output_to_stream + proc.redirect_error_to_stream + + -- Launch CGI execution + proc.launch + if proc.launched then + -- Do not send the header to CGI script + -- value are passed via environment variables + -- proc.put_string (l_input_header) + -- Send payload. + proc.put_string (l_input_buf) + + create l_output.make_empty + create l_error.make_empty + get_output_and_error_from_process (proc, l_output, l_error) + if l_error /= Void and then not l_error.is_whitespace then + res.put_error (l_error) + end + + -- Wait for process exit + if not proc.has_exited then + proc.wait_for_exit + end + if proc.exit_code /= 0 then + res.set_status_code ({HTTP_STATUS_CODE}.internal_server_error) + s := "CGI script execution failed [exit code=" + proc.exit_code.out+ "]!" + res.header.put_content_type_utf_8_text_plain + res.header.put_content_length (s.count) + res.put_string (s) + else + -- Send the response + -- error already sent via `res.put_error (l_error)` + from + i := 1 + n := l_output.count + until + i > n or l_output_header_sent + loop + j := l_output.index_of ('%N', i) + if j > 0 then + s := l_output.substring (i, j) + s.right_adjust + if s.is_empty then + -- Reached end of header + l_output_header_sent := True + else + res.add_header_line (s) + end + else + -- ERROR + l_output_header_sent := True + end + i := j + 1 + end + if l_output_header_sent then + if i <= n then + res.put_string (l_output.substring (i, n)) + end + else + res.set_status_code ({HTTP_STATUS_CODE}.internal_server_error) + s := "Internal server error!" + res.header.put_content_type_utf_8_text_plain + res.header.put_content_length (s.count) + res.put_string (s) + end + end + else + res.set_status_code ({HTTP_STATUS_CODE}.internal_server_error) + s := "Could not launch CGI script!" + res.header.put_content_type_utf_8_text_plain + res.header.put_content_length (s.count) + res.put_string (s) + end + else + res.set_status_code ({HTTP_STATUS_CODE}.not_found) + s := "Not found!" + res.header.put_content_type_utf_8_text_plain + res.header.put_content_length (s.count) + res.put_string (s) + end + end + + get_output_and_error_from_process (proc: BASE_PROCESS; a_output: STRING; a_error: STRING) + local + output_buf, error_buf: SPECIAL [NATURAL_8] + do + from + create output_buf.make_filled (0, buffer_size) + create error_buf.make_filled (0, buffer_size) + until + not attached output_buf and not attached error_buf + loop + if attached output_buf then + if proc.has_output_stream_error or proc.has_output_stream_closed then + output_buf := Void + end + if attached output_buf then + -- Try reading from standard output. + proc.read_output_to_special (output_buf) + across + output_buf as ic + loop + a_output.append_code (ic.item) + end + end + end + if attached output_buf implies output_buf.count = 0 and then attached error_buf then + -- Nothing is read from standard output, switch to standard error. + if proc.has_error_stream_error or proc.has_error_stream_closed then + error_buf := Void + end + if attached error_buf then + -- Try reading from standard error. + proc.read_error_to_special (error_buf) + across + error_buf as ic + loop + a_error.append_code (ic.item) + end + end + end + if attached output_buf then + output_buf.extend_filled (0) + end + if attached error_buf then + error_buf.extend_filled (0) + end + end + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Colin Adams, 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/wsf_extension.ecf b/library/server/wsf/wsf_extension.ecf index ddc5a1dc..0cb1459e 100644 --- a/library/server/wsf/wsf_extension.ecf +++ b/library/server/wsf/wsf_extension.ecf @@ -3,9 +3,9 @@ + /EIFGENs$ /\.git$ /\.svn$ - /EIFGENs$ @@ -13,6 +13,7 @@ + diff --git a/tools/httpd/README.md b/tools/httpd/README.md new file mode 100644 index 00000000..7e1bf3b5 --- /dev/null +++ b/tools/httpd/README.md @@ -0,0 +1,12 @@ +Simple web server +================= + +This server is very simple and limited, it is, for now: +- a file server +- a CGI handler (for `*.cgi` executables) + +Usage: + httpd (--root path) + + --root : document directory for file server (default: www) + diff --git a/tools/httpd/httpd.ecf b/tools/httpd/httpd.ecf new file mode 100644 index 00000000..820d3930 --- /dev/null +++ b/tools/httpd/httpd.ecf @@ -0,0 +1,56 @@ + + + + + /CVS$ + /EIFGENs$ + /\.svn$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/httpd/server.ini b/tools/httpd/server.ini new file mode 100644 index 00000000..017186a2 --- /dev/null +++ b/tools/httpd/server.ini @@ -0,0 +1,9 @@ +verbose=true +verbose_level=ALERT +port=9090 +#max_concurrent_connections=100 +#keep_alive_timeout=5 +#max_keep_alive_requests=300 +#max_tcp_clients=100 +#socket_timeout=60 +#socket_recv_timeout=15 diff --git a/tools/httpd/src/application.e b/tools/httpd/src/application.e new file mode 100644 index 00000000..9c1fe18a --- /dev/null +++ b/tools/httpd/src/application.e @@ -0,0 +1,31 @@ +note + description : "simple application root class" + date : "$Date$" + revision : "$Revision$" + +class + APPLICATION + +inherit + WSF_DEFAULT_SERVICE [APPLICATION_EXECUTION] + redefine + initialize + end + + SHARED_EXECUTION_ENVIRONMENT + +create + make_and_launch + +feature {NONE} -- Initialization + + initialize + -- Initialize current service. + do + -- Specific to `standalone' connector (the EiffelWeb server). + -- See `{WSF_STANDALONE_SERVICE_LAUNCHER}.initialize' + set_service_option ("port", 9090) + import_service_options (create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI}.make_from_file ("server.ini")) + end + +end diff --git a/tools/httpd/src/application_execution.e b/tools/httpd/src/application_execution.e new file mode 100644 index 00000000..211ce642 --- /dev/null +++ b/tools/httpd/src/application_execution.e @@ -0,0 +1,95 @@ +note + description : "simple application execution" + date : "$Date$" + revision : "$Revision$" + +class + APPLICATION_EXECUTION + +inherit + WSF_ROUTED_EXECUTION + + SHARED_EXECUTION_ENVIRONMENT + +create + make + +feature -- Setup + + cgi_file_extensions: ITERABLE [READABLE_STRING_GENERAL] + once + Result := <<"cgi">> + end + + directory_index_file_names: ITERABLE [READABLE_STRING_GENERAL] + once + Result := <<"index.html">> + end + + document_location: PATH + -- Root folder for the httpd files server. + local + i,n: INTEGER + d: detachable READABLE_STRING_GENERAL + once + Result := execution_environment.current_working_path.extended ("www") + if attached execution_environment.arguments as args then + from + i := 1 + n := args.argument_count + until + i > n or d /= Void + loop + if + attached args.argument (i) as v and then + v.same_string_general ("--root") and then + i < n + then + i := i + 1 + d := args.argument (i) + end + i := i + 1 + end + if d /= Void then + create Result.make_from_string (d) + end + end + end + + setup_router + -- Setup `router' + local + cgi: WSF_CGI_HANDLER + cgi_cond: WSF_ROUTING_CONDITION + fs: WSF_FILE_SYSTEM_HANDLER + m: WSF_STARTS_WITH_MAPPING + cond: WSF_WITH_CONDITION_MAPPING + s: STRING_32 + do + create cgi.make (document_location) + cgi_cond := create {WSF_ROUTING_FILE_EXISTS_CONDITION}.make (document_location) and create {WSF_ROUTING_EXTENSION_CONDITION}.make (cgi_file_extensions) + create cond.make (cgi_cond, cgi) + create s.make_empty + across + cgi_file_extensions as ic + loop + if not s.is_empty then + s.append_string_general (", ") + end + s.append_string_general ("*.") + s.append_string_general (ic.item) + end + cond.set_condition_description (s) + + router.map (cond, Void) + + create fs.make_with_path (document_location) + fs.enable_index + fs.set_directory_index (directory_index_file_names) + fs.set_not_found_handler (agent (i_uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE) do execute_default (req, res) end) + + create m.make ("", fs) + router.map (m, router.methods_get) + end + +end diff --git a/tools/httpd/www/home.html b/tools/httpd/www/home.html new file mode 100644 index 00000000..3903a1df --- /dev/null +++ b/tools/httpd/www/home.html @@ -0,0 +1 @@ +Hello EiffelWeb httpd server.