From ff57d0ecd429a774f190d81a87707a7b44d0b59c Mon Sep 17 00:00:00 2001 From: Olivier Ligot Date: Wed, 9 Jan 2013 17:34:50 +0100 Subject: [PATCH] Cross-Origin Resource Sharing initial support Initial support for the Cross-Origin Resource Sharing specification. This allows JavaScript to make requests across domain boundaries. Also reviewed the filter example to get rid of the context and the generic classes (we can actually use {WSF_REQUEST}.execution_variable and {WSF_REQUEST}.set_execution_variable). Links: * How to enable server-side: http://enable-cors.org/server.html * Specification: http://www.w3.org/TR/cors/ * Github: http://developer.github.com/v3/#cross-origin-resource-sharing --- examples/filter/filter-safe.ecf | 25 +++--- .../filter/src/filter/authentication_filter.e | 12 +-- examples/filter/src/filter_server.e | 83 +++++++++++-------- examples/filter/src/resource/user_handler.e | 22 ++--- .../network/protocol/http/src/http_header.e | 63 ++++++++++---- .../protocol/http/src/http_header_names.e | 19 ++++- .../wsf/router/filter/wsf_cors_filter.e | 33 ++++++++ .../router/filter/wsf_cors_options_filter.e | 59 +++++++++++++ .../src/response/wsf_cors_options_response.e | 64 ++++++++++++++ library/server/wsf/src/wsf_response.e | 26 +++++- 10 files changed, 324 insertions(+), 82 deletions(-) create mode 100644 library/server/wsf/router/filter/wsf_cors_filter.e create mode 100644 library/server/wsf/router/filter/wsf_cors_options_filter.e create mode 100644 library/server/wsf/src/response/wsf_cors_options_response.e diff --git a/examples/filter/filter-safe.ecf b/examples/filter/filter-safe.ecf index ade4f80f..6c86cb8e 100644 --- a/examples/filter/filter-safe.ecf +++ b/examples/filter/filter-safe.ecf @@ -12,22 +12,23 @@ - - + + + + - - - - - - - - - - + + + + + + + + + diff --git a/examples/filter/src/filter/authentication_filter.e b/examples/filter/src/filter/authentication_filter.e index 9b407c7b..a18da1e7 100644 --- a/examples/filter/src/filter/authentication_filter.e +++ b/examples/filter/src/filter/authentication_filter.e @@ -8,9 +8,9 @@ class AUTHENTICATION_FILTER inherit - WSF_FILTER_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] + WSF_FILTER - WSF_URI_TEMPLATE_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] + WSF_URI_TEMPLATE_HANDLER SHARED_DATABASE_API @@ -18,7 +18,7 @@ inherit feature -- Basic operations - execute (ctx: FILTER_HANDLER_CONTEXT; req: WSF_REQUEST; res: WSF_RESPONSE) + execute (req: WSF_REQUEST; res: WSF_RESPONSE) -- Execute the filter local l_auth: HTTP_AUTHORIZATION @@ -31,8 +31,8 @@ feature -- Basic operations attached l_auth.password as l_auth_password and then l_auth_password.same_string (l_user.password) then - ctx.set_user (l_user) - execute_next (ctx, req, res) + req.set_execution_variable ("user", l_user) + execute_next (req, res) else handle_unauthorized ("Unauthorized", req, res) end @@ -56,6 +56,6 @@ feature {NONE} -- Implementation end note - copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others" + copyright: "2011-2013, Olivier Ligot, Jocelyn Fiat and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/examples/filter/src/filter_server.e b/examples/filter/src/filter_server.e index 25d16fef..7368581d 100644 --- a/examples/filter/src/filter_server.e +++ b/examples/filter/src/filter_server.e @@ -24,39 +24,60 @@ create feature {NONE} -- Initialization make + local + l_message: STRING + l_factory: INET_ADDRESS_FACTORY do + create router.make (1) initialize_filter initialize_json - set_service_option ("port", 9090) + set_service_option ("port", port) + create l_message.make_empty + l_message.append_string ("Launching filter server at ") + create l_factory + l_message.append_string (l_factory.create_localhost.host_name) + l_message.append_string (" port ") + l_message.append_integer (port) + io.put_string (l_message) + io.put_new_line make_and_launch end create_filter -- Create `filter' local - l_router: WSF_ROUTER - l_authentication_filter_hdl: AUTHENTICATION_FILTER - l_user_filter: USER_HANDLER - l_routing_filter: WSF_ROUTING_FILTER + l_cors_filter: WSF_CORS_FILTER do - create l_router.make (1) - create l_authentication_filter_hdl - create l_user_filter - l_authentication_filter_hdl.set_next (l_user_filter) - - l_router.handle_with_request_methods ("/user/{userid}", l_authentication_filter_hdl, l_router.methods_get) - create l_routing_filter.make (l_router) - l_routing_filter.set_execute_default_action (agent execute_default) - filter := l_routing_filter + create l_cors_filter + filter := l_cors_filter end setup_filter -- Setup `filter' local + l_options_filter: WSF_CORS_OPTIONS_FILTER + l_authentication_filter: AUTHENTICATION_FILTER + l_user_filter: USER_HANDLER + l_methods: WSF_REQUEST_METHODS + l_routing_filter: WSF_ROUTING_FILTER l_logging_filter: WSF_LOGGING_FILTER do + create l_options_filter.make (router) + create l_authentication_filter + l_options_filter.set_next (l_authentication_filter) + create l_user_filter + l_authentication_filter.set_next (l_user_filter) + + create l_methods + l_methods.enable_options + l_methods.enable_get + router.handle_with_request_methods ("/user/{userid}", l_options_filter, l_methods) + create l_routing_filter.make (router) + l_routing_filter.set_execute_default_action (agent execute_default) + filter.set_next (l_routing_filter) + create l_logging_filter - filter.set_next (l_logging_filter) + l_routing_filter.set_next (l_logging_filter) end initialize_json @@ -73,30 +94,24 @@ feature -- Basic operations end execute_default (req: WSF_REQUEST; res: WSF_RESPONSE) - -- I'm using this method to handle the method not allowed response - -- in the case that the given uri does not have a corresponding http method - -- to handle it. local - h : HTTP_HEADER - l_description : STRING - l_api_doc : STRING + l_message: WSF_DEFAULT_ROUTER_RESPONSE do - if req.content_length_value > 0 then - req.input.read_string (req.content_length_value.as_integer_32) - end - create h.make - h.put_content_type_text_plain - l_api_doc := "%NPlease check the API%NURI:/user/{userid} METHOD: GET%N" - l_description := req.request_method + req.request_uri + " is not allowed" + "%N" + l_api_doc - h.put_content_length (l_description.count) - h.put_current_date - res.set_status_code ({HTTP_STATUS_CODE}.method_not_allowed) - res.put_header_text (h.string) - res.put_string (l_description) + create l_message.make_with_router (req, router) + l_message.set_documentation_included (True) + res.send (l_message) end +feature {NONE} -- Implementation + + port: INTEGER = 9090 + -- Port number + + router: WSF_ROUTER; + -- Router + note - copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others" + copyright: "2011-2013, Olivier Ligot, Jocelyn Fiat and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/examples/filter/src/resource/user_handler.e b/examples/filter/src/resource/user_handler.e index 223e93d5..722ccd03 100644 --- a/examples/filter/src/resource/user_handler.e +++ b/examples/filter/src/resource/user_handler.e @@ -8,11 +8,11 @@ class USER_HANDLER inherit - WSF_FILTER_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] + WSF_FILTER - WSF_URI_TEMPLATE_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] + WSF_URI_TEMPLATE_HANDLER - WSF_RESOURCE_CONTEXT_HANDLER_HELPER [FILTER_HANDLER_CONTEXT] + WSF_RESOURCE_HANDLER_HELPER redefine do_get end @@ -23,30 +23,30 @@ inherit feature -- Basic operations - execute (ctx: FILTER_HANDLER_CONTEXT; req: WSF_REQUEST; res: WSF_RESPONSE) + execute (req: WSF_REQUEST; res: WSF_RESPONSE) -- Execute request handler do - execute_methods (ctx, req, res) - execute_next (ctx, req, res) + execute_methods (req, res) + execute_next (req, res) end - do_get (ctx: FILTER_HANDLER_CONTEXT; req: WSF_REQUEST; res: WSF_RESPONSE) + do_get (req: WSF_REQUEST; res: WSF_RESPONSE) -- Using GET to retrieve resource information. -- If the GET request is SUCCESS, we response with -- 200 OK, and a representation of the user -- If the GET request is not SUCCESS, we response with -- 404 Resource not found require else - authenticated_user_attached: attached ctx.user + authenticated_user_attached: attached {USER} req.execution_variable ("user") local id : STRING do if attached req.orig_path_info as orig_path then id := get_user_id_from_path (orig_path) if attached retrieve_user (id) as l_user then - if l_user ~ ctx.user then + if l_user ~ req.execution_variable ("user") then compute_response_get (req, res, l_user) - elseif attached ctx.user as l_auth_user then + elseif attached {USER} req.execution_variable ("user") as l_auth_user then -- Trying to access another user that the authenticated one, -- which is forbidden in this example... handle_forbidden ("You try to access the user " + id.out + " while authenticating with the user " + l_auth_user.id.out, req, res) @@ -92,6 +92,6 @@ feature {NONE} -- Implementation end note - copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others" + copyright: "2011-2013, Olivier Ligot, Jocelyn Fiat and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/library/network/protocol/http/src/http_header.e b/library/network/protocol/http/src/http_header.e index 001067a9..4e89707e 100644 --- a/library/network/protocol/http/src/http_header.e +++ b/library/network/protocol/http/src/http_header.e @@ -213,6 +213,25 @@ feature -- Header change: general put_header (k + colon_space + v) end + put_header_key_methods (k: READABLE_STRING_8; a_methods: ITERABLE [READABLE_STRING_8]) + -- Add header `k: a_methods', or replace existing header of same header methods/key + local + s: STRING_8 + do + create s.make_empty + across + a_methods as c + loop + if not s.is_empty then + s.append_string (", ") + end + s.append (c.item) + end + if not s.is_empty then + put_header_key_value (k, s) + end + end + feature -- Content related header put_content_type (t: READABLE_STRING_8) @@ -397,26 +416,38 @@ feature -- Content-type helpers put_content_type_multipart_encrypted do put_content_type ({HTTP_MIME_TYPES}.multipart_encrypted) end put_content_type_application_x_www_form_encoded do put_content_type ({HTTP_MIME_TYPES}.application_x_www_form_encoded) end +feature -- Cross-Origin Resource Sharing + + put_access_control_allow_origin (s: READABLE_STRING_8) + -- Put "Access-Control-Allow-Origin" header. + do + put_header_key_value ({HTTP_HEADER_NAMES}.header_access_control_allow_origin, s) + end + + put_access_control_allow_all_origin + -- Put "Access-Control-Allow-Origin: *" header. + do + put_access_control_allow_origin ("*") + end + + put_access_control_allow_methods (a_methods: ITERABLE [READABLE_STRING_8]) + -- If `a_methods' is not empty, put `Access-Control-Allow-Methods' header with list `a_methods' of methods + do + put_header_key_methods ({HTTP_HEADER_NAMES}.header_access_control_allow_methods, a_methods) + end + + put_access_control_allow_headers (s: READABLE_STRING_8) + -- Put "Access-Control-Allow-Headers" header. + do + put_header_key_value ({HTTP_HEADER_NAMES}.header_access_control_allow_headers, s) + end + feature -- Method related put_allow (a_methods: ITERABLE [READABLE_STRING_8]) -- If `a_methods' is not empty, put `Allow' header with list `a_methods' of methods - local - s: STRING_8 do - create s.make_empty - across - a_methods as c - loop - if not s.is_empty then - s.append_character (',') - end - s.append_character (' ') - s.append (c.item) - end - if not s.is_empty then - put_header_key_value ({HTTP_HEADER_NAMES}.header_allow, s) - end + put_header_key_methods ({HTTP_HEADER_NAMES}.header_allow, a_methods) end feature -- Date @@ -738,7 +769,7 @@ feature {NONE} -- Constants semi_colon_space: STRING = "; " note - copyright: "2011-2012, Jocelyn Fiat, Eiffel Software and others" + copyright: "2011-2013, Jocelyn Fiat, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/network/protocol/http/src/http_header_names.e b/library/network/protocol/http/src/http_header_names.e index 8d679ea9..4bab76e8 100644 --- a/library/network/protocol/http/src/http_header_names.e +++ b/library/network/protocol/http/src/http_header_names.e @@ -194,6 +194,23 @@ feature -- Response header name -- Indicates the authentication scheme that should be used to access the requested entity. --| Example: WWW-Authenticate: Basic +feature -- Cross-Origin Resource Sharing + + header_access_control_allow_origin: STRING = "Access-Control-Allow-Origin" + -- Indicates whether a resource can be shared based by returning + -- the value of the Origin request header in the response. + -- | Example: Access-Control-Allow-Origin: http://example.org + + header_access_control_allow_methods: STRING = "Access-Control-Allow-Methods" + -- Indicates, as part of the response to a preflight request, + -- which methods can be used during the actual request. + -- | Example: Access-Control-Allow-Methods: PUT, DELETE + + header_access_control_allow_headers: STRING = "Access-Control-Allow-Headers" + -- Indicates, as part of the response to a preflight request, + -- which header field names can be used during the actual request. + -- | Example: Access-Control-Allow-Headers: Authorization + feature -- Request or Response header name header_cache_control: STRING = "Cache-Control" @@ -248,7 +265,7 @@ feature -- MIME related header_content_transfer_encoding: STRING = "Content-Transfer-Encoding" note - copyright: "2011-2012, Jocelyn Fiat, Eiffel Software and others" + copyright: "2011-2013, Jocelyn Fiat, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software diff --git a/library/server/wsf/router/filter/wsf_cors_filter.e b/library/server/wsf/router/filter/wsf_cors_filter.e new file mode 100644 index 00000000..b306ad60 --- /dev/null +++ b/library/server/wsf/router/filter/wsf_cors_filter.e @@ -0,0 +1,33 @@ +note + description: "Cross-Origin Resource Sharing filter." + author: "Olivier Ligot" + date: "$Date$" + revision: "$Revision$" + EIS: "name=Cross-Origin Resource Sharing", "src=http://www.w3.org/TR/cors/", "tag=W3C" + +class + WSF_CORS_FILTER + +inherit + WSF_FILTER + +feature -- Basic operations + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute the filter. + do + res.header.put_access_control_allow_all_origin + execute_next (req, res) + end + +note + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, 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/router/filter/wsf_cors_options_filter.e b/library/server/wsf/router/filter/wsf_cors_options_filter.e new file mode 100644 index 00000000..7ca0bd87 --- /dev/null +++ b/library/server/wsf/router/filter/wsf_cors_options_filter.e @@ -0,0 +1,59 @@ +note + description: "Filter that handles an OPTIONS request, with Cross-Origin Resource Sharing support." + author: "Olvier Ligot" + date: "$Date$" + revision: "$Revision$" + EIS: "name=Cross-Origin Resource Sharing", "src=http://www.w3.org/TR/cors/", "tag=W3C" + +class + WSF_CORS_OPTIONS_FILTER + +inherit + WSF_FILTER + + WSF_URI_TEMPLATE_HANDLER + +create + make + +feature {NONE} -- Initialization + + make (a_router: like router) + -- Initialize Current with `a_router'. + do + router := a_router + ensure + router_set: router = a_router + end + +feature -- Access + + router: WSF_ROUTER + -- Associated router + +feature -- Basic operations + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute the filter. + local + msg: WSF_CORS_OPTIONS_RESPONSE + do + if req.is_request_method ({HTTP_REQUEST_METHODS}.method_options) then + create msg.make (req, router) + res.send (msg) + else + execute_next (req, res) + end + end + +note + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, 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/src/response/wsf_cors_options_response.e b/library/server/wsf/src/response/wsf_cors_options_response.e new file mode 100644 index 00000000..a0790a0b --- /dev/null +++ b/library/server/wsf/src/response/wsf_cors_options_response.e @@ -0,0 +1,64 @@ +note + description: "Response to an OPTIONS request, with Cross-Origin Resource Sharing support." + author: "Olivier Ligt" + date: "$Date$" + revision: "$Revision$" + EIS: "name=Cross-Origin Resource Sharing", "src=http://www.w3.org/TR/cors/", "tag=W3C" + +class + WSF_CORS_OPTIONS_RESPONSE + +inherit + WSF_RESPONSE_MESSAGE + +create + make + +feature {NONE} -- Initialization + + make (req: WSF_REQUEST; a_router: like router) + do + request := req + router := a_router + create header.make + end + +feature -- Access + + request: WSF_REQUEST + -- Associated request + + router: WSF_ROUTER + -- Associated router + + header: HTTP_HEADER + -- Response' header + +feature {WSF_RESPONSE} -- Output + + send_to (res: WSF_RESPONSE) + local + l_methods: WSF_REQUEST_METHODS + do + res.set_status_code ({HTTP_STATUS_CODE}.No_content) + header.put_current_date + header.put_access_control_allow_headers ({HTTP_HEADER_NAMES}.header_authorization) + l_methods := router.allowed_methods_for_request (request) + if not l_methods.is_empty then + header.put_allow (l_methods) + header.put_access_control_allow_methods (l_methods) + end + res.put_header_text (header.string) + end + +note + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, 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/src/wsf_response.e b/library/server/wsf/src/wsf_response.e index edd69fed..81d1e208 100644 --- a/library/server/wsf/src/wsf_response.e +++ b/library/server/wsf/src/wsf_response.e @@ -31,6 +31,7 @@ feature {NONE} -- Initialization do transfered_content_length := 0 wgi_response := r + create header.make end feature {WSF_RESPONSE_EXPORTER} -- Properties @@ -114,6 +115,13 @@ feature -- Status setting feature -- Header output operation + header: HTTP_HEADER + -- Header + -- This is useful when we want to fill the `header' + -- in two pass (i.e. in two different classes). + -- We first call features of `header', and finally + -- we call `put_header_text' + put_header_text (a_text: READABLE_STRING_8) -- Sent `a_text' and just before send the status code require @@ -121,9 +129,23 @@ feature -- Header output operation header_not_committed: not header_committed a_text_ends_with_single_crlf: a_text.count > 2 implies not a_text.substring (a_text.count - 2, a_text.count).same_string ("%R%N") a_text_does_not_end_with_double_crlf: a_text.count > 4 implies not a_text.substring (a_text.count - 4, a_text.count).same_string ("%R%N%R%N") + local + l_text: READABLE_STRING_8 + l_header: HTTP_HEADER do wgi_response.set_status_code (status_code, status_reason_phrase) - wgi_response.put_header_text (a_text) + if header.is_empty then + l_text := a_text + else + create l_header.make_from_raw_header_data (a_text) + across + l_header as c + loop + header.put_header (c.item.string) + end + l_text := header.string + end + wgi_response.put_header_text (l_text) ensure status_set: status_is_set status_committed: status_committed @@ -376,7 +398,7 @@ feature -- Error reporting end note - copyright: "2011-2012, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others" + copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" source: "[ Eiffel Software