diff --git a/examples/restbucksCRUD/restbucks-safe.ecf b/examples/restbucksCRUD/restbucks-safe.ecf index 86dcda7f..08715841 100644 --- a/examples/restbucksCRUD/restbucks-safe.ecf +++ b/examples/restbucksCRUD/restbucks-safe.ecf @@ -1,5 +1,5 @@ - + @@ -18,11 +18,14 @@ + + + diff --git a/examples/restbucksCRUD/src/resource/order_handler.e b/examples/restbucksCRUD/src/resource/order_handler.e index 0bddd924..91d1b9ae 100644 --- a/examples/restbucksCRUD/src/resource/order_handler.e +++ b/examples/restbucksCRUD/src/resource/order_handler.e @@ -5,17 +5,10 @@ note revision: "$Revision$" class ORDER_HANDLER + inherit - WSF_URI_TEMPLATE_HANDLER - - WSF_RESOURCE_HANDLER_HELPER - redefine - do_get, - do_post, - do_put, - do_delete - end + WSF_SKELETON_HANDLER SHARED_DATABASE_API @@ -25,154 +18,394 @@ inherit SHARED_ORDER_VALIDATION - WSF_SELF_DOCUMENTED_HANDLER - -feature -- Execute - - execute (req: WSF_REQUEST; res: WSF_RESPONSE) - -- Execute request handler - do - execute_methods (req, res) + WSF_RESOURCE_HANDLER_HELPER + rename + execute_options as helper_execute_options, + handle_internal_server_error as helper_handle_internal_server_error end -feature -- API DOC +create - api_doc : STRING = "URI:/order METHOD: POST%N URI:/order/{orderid} METHOD: GET, PUT, DELETE%N" + make_with_router +feature -- Execution variables + + Order_execution_variable: STRING = "ORDER" + -- Execution variable used by application + + Generated_content_execution_variable: STRING = "GENERATED_CONTENT" + -- Execution variable used by application + + Extracted_order_execution_variable: STRING = "EXTRACTED_ORDER" + -- Execution variable used by application + feature -- Documentation - mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION + description: READABLE_STRING_GENERAL + -- General description for self-generated documentation; + -- The specific URI templates supported will be described automatically do - create Result.make (m) - if a_request_methods /= Void then - if a_request_methods.has_method_post then - Result.add_description ("URI:/order METHOD: POST") - elseif - a_request_methods.has_method_get - or a_request_methods.has_method_put - or a_request_methods.has_method_delete - then - Result.add_description ("URI:/order/{orderid} METHOD: GET, PUT, DELETE") + Result := "Create, Read, Update or Delete an ORDER." + end + +feature -- Access + + is_chunking (req: WSF_REQUEST): BOOLEAN + -- Will the response to `req' using chunked transfer encoding? + do + -- No. + end + + includes_response_entity (req: WSF_REQUEST): BOOLEAN + -- Does the response to `req' include an entity? + -- Method will be DELETE, POST, PUT or an extension method. + do + Result := False + -- At present, there is no support for this except for DELETE. + end + + conneg (req: WSF_REQUEST): CONNEG_SERVER_SIDE + -- Content negotiatior for all requests + once + create Result.make ({HTTP_MIME_TYPES}.application_json, "en", "UTF-8", "identity") + end + + mime_types_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept header that `Current' can serve + do + create {ARRAYED_LIST [STRING]} Result.make_from_array (<<{HTTP_MIME_TYPES}.application_json>>) + Result.compare_objects + end + + languages_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Language header that `Current' can serve + do + create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"en">>) + Result.compare_objects + end + + charsets_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Charset header that `Current' can serve + do + create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"UTF-8">>) + Result.compare_objects + end + + encodings_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Encoding header that `Current' can serve + do + create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"identity">>) + Result.compare_objects + end + + max_age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale; + -- This is used to generate a Cache-Control: max-age header. + -- Return 0 to indicate already expired. + -- Return Never_expires to indicate never expires. + do + -- All our responses are considered stale. + end + + is_freely_cacheable (req: WSF_REQUEST): BOOLEAN + -- Should the response to `req' be freely cachable in shared caches? + -- If `True', then a Cache-Control: public header will be generated. + do + -- definitely not! + end + + private_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8] + -- Header names intended for a single user. + -- If non-Void, then a Cache-Control: private header will be generated. + -- Returning an empty list prevents the entire response from being served from a shared cache. + do + create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0) + end + + non_cacheable_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8] + -- Header names that will not be sent from a cache without revalidation; + -- If non-Void, then a Cache-Control: no-cache header will be generated. + -- Returning an empty list prevents the response being served from a cache + -- without revalidation. + do + create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0) + end + + is_sensitive (req: WSF_REQUEST): BOOLEAN + -- Is the response to `req' of a sensitive nature? + -- If `True' then a Cache-Control: no-store header will be generated. + do + Result := True + -- since it's commercial data. + end + + matching_etag (req: WSF_REQUEST; a_etag: READABLE_STRING_32; a_strong: BOOLEAN): BOOLEAN + -- Is `a_etag' a match for resource requested in `req'? + -- If `a_strong' then the strong comparison function must be used. + local + l_id: STRING + l_etag_util: ETAG_UTILS + do + l_id := order_id_from_request (req) + if db_access.orders.has_key (l_id) then + check attached db_access.orders.item (l_id) as l_order then + -- postcondition of `has_key' + create l_etag_util + Result := a_etag.same_string (l_etag_util.md5_digest (l_order.out).as_string_32) end end end + etag (req: WSF_REQUEST): detachable READABLE_STRING_8 + -- Optional Etag for `req' in the requested variant + local + l_etag_utils: ETAG_UTILS + do + create l_etag_utils + if attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then + Result := l_etag_utils.md5_digest (l_order.out) + end + end + + modified_since (req: WSF_REQUEST; a_date_time: DATE_TIME): BOOLEAN + -- Has resource requested in `req' been modified since `a_date_time' (UTC)? + do + -- We don't track this information. It is safe to always say yes. + Result := True + end + +feature -- Measurement + + content_length (req: WSF_REQUEST): NATURAL + -- Length of entity-body of the response to `req' + do + check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then + -- postcondition generated_content_set_for_get_head of `ensure_content_available' + -- We only call this for GET/HEAD in this example. + Result := l_response.count.as_natural_32 + end + end + + allow_post_to_missing_resource (req: WSF_REQUEST): BOOLEAN + -- The resource named in `req' does not exist, and this is a POST. Do we allow it? + do + -- No. + end + +feature -- Status report + + finished (req: WSF_REQUEST): BOOLEAN + -- Has the last chunk been generated for `req'? + do + -- precondition is never met + end + +feature -- Execution + + check_resource_exists (req: WSF_REQUEST; a_helper: WSF_METHOD_HELPER) + -- Call `a_helper.set_resource_exists' to indicate that `req.path_translated' + -- is the name of an existing resource. + -- We also put the order into `req.execution_variable (Order_execution_variable)' for GET or HEAD responses. + local + l_id: STRING + do + if req.is_post_request_method then + a_helper.set_resource_exists + -- because only /order is defined to this handler for POST + else + -- the request is of the form /order/{orderid} + l_id := order_id_from_request (req) + if db_access.orders.has_key (l_id) then + a_helper.set_resource_exists + if req.is_get_head_request_method then + check attached db_access.orders.item (l_id) as l_order then + -- postcondition `item_if_found' of `has_key' + req.set_execution_variable (Order_execution_variable, l_order) + end + end + end + end + ensure then + order_saved_only_for_get_head: req.is_get_head_request_method = + attached {ORDER} req.execution_variable (Order_execution_variable) + end + +feature -- GET/HEAD content + + ensure_content_available (req: WSF_REQUEST) + -- Commence generation of response text (entity-body). + -- If not chunked, then this will create the entire entity-body so as to be available + -- for a subsequent call to `content'. + -- If chunked, only the first chunk will be made available to `next_chunk'. If chunk extensions + -- are used, then this will also generate the chunk extension for the first chunk. + -- We save the text in `req.execution_variable (Generated_content_execution_variable)' + -- We ignore the results of content negotiation, as there is only one possible combination. + do + check attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then + -- precondition get_or_head and postcondition order_saved_only_for_get_head of `check_resource_exists' and + if attached {JSON_VALUE} json.value (l_order) as jv then + req.set_execution_variable (Generated_content_execution_variable, jv.representation) + else + req.set_execution_variable (Generated_content_execution_variable, "") + end + end + ensure then + generated_content_set_for_get_head: req.is_get_head_request_method implies + attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) + end + + content (req: WSF_REQUEST): READABLE_STRING_8 + -- Non-chunked entity body in response to `req'; + -- We only call this for GET/HEAD in this example. + do + check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then + -- postcondition generated_content_set_for_get_head of `ensure_content_available' + Result := l_response + end + end + + next_chunk (req: WSF_REQUEST): TUPLE [a_chunk: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8] + -- Next chunk of entity body in response to `req'; + -- The second field of the result is an optional chunk extension. + do + -- precondition `is_chunking' is never met, but we need a dummy `Result' + -- to satisfy the compiler in void-safe mode + Result := ["", Void] + end + + generate_next_chunk (req: WSF_REQUEST) + -- Prepare next chunk (including optional chunk extension) of entity body in response to `req'. + -- This is not called for the first chunk. + do + -- precondition `is_chunking' is never met + end + +feature -- DELETE + + delete (req: WSF_REQUEST) + -- Delete resource named in `req' or set an error on `req.error_handler'. + local + l_id: STRING + do + l_id := order_id_from_request (req) + if db_access.orders.has_key (l_id) then + if is_valid_to_delete (l_id) then + delete_order (l_id) + else + req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.method_not_allowed, "DELETE not valid", + "There is conflict while trying to delete the order, the order could not be deleted in the current state") + end + else + req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.not_found, "DELETE not valid", + "There is no such order to delete") + end + end + + delete_queued (req: WSF_REQUEST): BOOLEAN + -- Has resource named by `req' been queued for deletion? + do + -- No + end + + +feature -- PUT/POST + + is_entity_too_large (req: WSF_REQUEST): BOOLEAN + -- Is the entity stored in `req.execution_variable (Request_entity_execution_variable)' too large for the application? + do + -- No. We don't care for this example. + end + + check_content_headers (req: WSF_REQUEST) + -- Check we can support all content headers on request entity. + -- Set `req.execution_variable (Content_check_code_execution_variable)' to {NATURAL} zero if OK, or 415 or 501 if not. + do + -- We don't bother for this example. Note that this is equivalent to setting zero. + end + + create_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Create new resource in response to a PUT request when `check_resource_exists' returns `False'. + -- Implementor must set error code of 200 OK or 500 Server Error. + do + -- We don't support creating a new resource with PUT. But this can't happen + -- with our router mappings, so we don't bother to set a 500 response. + end + + append_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Create new resource in response to a POST request. + -- Implementor must set error code of 200 OK or 204 No Content or 303 See Other or 500 Server Error. + do + if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then + save_order (l_order) + compute_response_post (req, res, l_order) + else + handle_bad_request_response ("Not a valid order", req, res) + end + end + + check_conflict (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Check we can support all content headers on request entity. + -- Set `req.execution_variable (Conflict_check_code_execution_variable)' to {NATURAL} zero if OK, or 409 if not. + -- In the latter case, write the full error response to `res'. + do + if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then + if not is_valid_to_update (l_order) then + req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409) + handle_resource_conflict_response (l_order.out +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res) + end + else + req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409) + --| This ought to be a 500, as if attached should probably be check attached. But as yet I lack a proof. + handle_resource_conflict_response ("There is conflict while trying to update the order, the order could not be update in the current state", req, res) + end + end + + check_request (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Check that the request entity is a valid request. + -- The entity is available as `req.execution_variable (Conflict_check_code_execution_variable)'. + -- Set `req.execution_variable (Request_check_code_execution_variable)' to {NATURAL} zero if OK, or 400 if not. + -- In the latter case, write the full error response to `res'. + local + l_order: detachable ORDER + l_id: STRING + do + if attached {READABLE_STRING_8} req.execution_variable (Request_entity_execution_variable) as l_request then + l_order := extract_order_request (l_request) + if req.is_put_request_method then + l_id := order_id_from_request (req) + if l_order /= Void and then db_access.orders.has_key (l_id) then + l_order.set_id (l_id) + req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0) + req.set_execution_variable (Extracted_order_execution_variable, l_order) + else + req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400) + handle_bad_request_response (l_request +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res) + end + else + req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0) + req.set_execution_variable (Extracted_order_execution_variable, l_order) + end + else + req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400) + handle_bad_request_response ("Request is not a valid ORDER", req, res) + end + end + + update_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Perform the update requested in `req'. + -- Write a response to `res' with a code of 204 or 500. + do + if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then + update_order (l_order) + compute_response_put (req, res, l_order) + else + handle_internal_server_error (res) + end + end + feature -- HTTP Methods - do_get (req: WSF_REQUEST; res: WSF_RESPONSE) - -- - local - id: STRING - do - if attached req.orig_path_info as orig_path then - id := get_order_id_from_path (orig_path) - if attached retrieve_order (id) as l_order then - if is_conditional_get (req, l_order) then - handle_resource_not_modified_response ("The resource" + orig_path + "does not change", req, res) - else - compute_response_get (req, res, l_order) - end - else - handle_resource_not_found_response ("The following resource" + orig_path + " is not found ", req, res) - end - end - end - - is_conditional_get (req : WSF_REQUEST; l_order : ORDER) : BOOLEAN - -- Check if If-None-Match is present and then if there is a representation that has that etag - -- if the representation hasn't changed, we return TRUE - -- then the response is a 304 with no entity body returned. - local - etag_util : ETAG_UTILS - do - if attached req.meta_string_variable ("HTTP_IF_NONE_MATCH") as if_none_match then - create etag_util - if if_none_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then - Result := True - end - end - end - - compute_response_get (req: WSF_REQUEST; res: WSF_RESPONSE; l_order: ORDER) - local - h: HTTP_HEADER - l_msg : STRING - etag_utils : ETAG_UTILS - do - create h.make - create etag_utils - h.put_content_type_application_json - if attached {JSON_VALUE} json.value (l_order) as jv then - l_msg := jv.representation - h.put_content_length (l_msg.count) - if attached req.request_time as time then - h.add_header ("Date:" + time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT") - end - h.add_header ("etag:" + etag_utils.md5_digest (l_order.out)) - res.set_status_code ({HTTP_STATUS_CODE}.ok) - res.put_header_text (h.string) - res.put_string (l_msg) - end - end - - do_put (req: WSF_REQUEST; res: WSF_RESPONSE) - -- Updating a resource with PUT - -- A successful PUT request will not create a new resource, instead it will - -- change the state of the resource identified by the current uri. - -- If success we response with 200 and the updated order. - -- 404 if the order is not found - -- 400 in case of a bad request - -- 500 internal server error - -- If the request is a Conditional PUT, and it does not mat we response - -- 415, precondition failed. - local - l_put: STRING - l_order : detachable ORDER - id : STRING - do - if attached req.orig_path_info as orig_path then - id := get_order_id_from_path (orig_path) - l_put := retrieve_data (req) - l_order := extract_order_request(l_put) - if l_order /= Void and then db_access.orders.has_key (id) then - l_order.set_id (id) - if is_valid_to_update(l_order) then - if is_conditional_put (req, l_order) then - update_order( l_order) - compute_response_put (req, res, l_order) - else - handle_precondition_fail_response ("", req, res) - end - else - --| FIXME: Here we need to define the Allow methods - handle_resource_conflict_response (l_put +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res) - end - else - handle_bad_request_response (l_put +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res) - end - end - end - - is_conditional_put (req : WSF_REQUEST; order : ORDER) : BOOLEAN - -- Check if If-Match is present and then if there is a representation that has that etag - -- if the representation hasn't changed, we return TRUE - local - etag_util : ETAG_UTILS - do - if attached retrieve_order (order.id) as l_order then - if attached req.meta_string_variable ("HTTP_IF_MATCH") as if_match then - create etag_util - if if_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then - Result := True - end - else - Result := True - end - end - end - - compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER) local h: HTTP_HEADER @@ -198,67 +431,6 @@ feature -- HTTP Methods end end - - do_delete (req: WSF_REQUEST; res: WSF_RESPONSE) - -- Here we use DELETE to cancel an order, if that order is in state where - -- it can still be canceled. - -- 200 if is ok - -- 404 Resource not found - -- 405 if consumer and service's view of the resouce state is inconsisent - -- 500 if we have an internal server error - local - id: STRING - do - if attached req.orig_path_info as orig_path then - id := get_order_id_from_path (orig_path) - if db_access.orders.has_key (id) then - if is_valid_to_delete (id) then - delete_order( id) - compute_response_delete (req, res) - else - --| FIXME: Here we need to define the Allow methods - handle_method_not_allowed_response (orig_path + "%N There is conflict while trying to delete the order, the order could not be deleted in the current state", req, res) - end - else - handle_resource_not_found_response (orig_path + " not found in this server", req, res) - end - end - end - - compute_response_delete (req: WSF_REQUEST; res: WSF_RESPONSE) - local - h : HTTP_HEADER - do - create h.make - h.put_content_type_application_json - if attached req.request_time as time then - h.put_utc_date (time) - end - res.set_status_code ({HTTP_STATUS_CODE}.no_content) - res.put_header_text (h.string) - end - - do_post (req: WSF_REQUEST; res: WSF_RESPONSE) - -- Here the convention is the following. - -- POST is used for creation and the server determines the URI - -- of the created resource. - -- If the request post is SUCCESS, the server will create the order and will response with - -- HTTP_RESPONSE 201 CREATED, the Location header will contains the newly created order's URI - -- if the request post is not SUCCESS, the server will response with - -- HTTP_RESPONSE 400 BAD REQUEST, the client send a bad request - -- HTTP_RESPONSE 500 INTERNAL_SERVER_ERROR, when the server can deliver the request - local - l_post: STRING - do - l_post := retrieve_data (req) - if attached extract_order_request (l_post) as l_order then - save_order (l_order) - compute_response_post (req, res, l_order) - else - handle_bad_request_response (l_post +"%N is not a valid ORDER", req, res) - end - end - compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER) local h: HTTP_HEADER @@ -290,9 +462,16 @@ feature -- HTTP Methods feature {NONE} -- URI helper methods - get_order_id_from_path (a_path: READABLE_STRING_32) : STRING + order_id_from_request (req: WSF_REQUEST): STRING + -- Value of "orderid" template URI variable in `req' + require + req_attached: req /= Void do - Result := a_path.split ('/').at (3) + if attached {WSF_VALUE} req.path_parameter ("orderid") as l_value then + Result := l_value.as_string.value.as_string_8 + else + Result := "" + end end feature {NONE} -- Implementation Repository Layer @@ -374,6 +553,6 @@ feature {NONE} -- Implementation Repository Layer end note - copyright: "2011-2012, Javier Velilla and others" + copyright: "2011-2013, Javier Velilla and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/examples/restbucksCRUD/src/restbucks_server.e b/examples/restbucksCRUD/src/restbucks_server.e index 81008346..588b2057 100644 --- a/examples/restbucksCRUD/src/restbucks_server.e +++ b/examples/restbucksCRUD/src/restbucks_server.e @@ -37,7 +37,7 @@ feature {NONE} -- Initialization order_handler: ORDER_HANDLER doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER do - create order_handler + create order_handler.make_with_router (router) router.handle_with_request_methods ("/order", order_handler, router.methods_POST) router.handle_with_request_methods ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT) create doc.make_hidden (router) diff --git a/library/network/protocol/http/src/http_header.e b/library/network/protocol/http/src/http_header.e index b1eee52d..d70ac062 100644 --- a/library/network/protocol/http/src/http_header.e +++ b/library/network/protocol/http/src/http_header.e @@ -350,7 +350,9 @@ feature -- Header change: general end add_header_key_value (k,v: READABLE_STRING_8) - -- Add header `k:v', or replace existing header of same header name/key + -- Add header `k:v'. + -- If it already exists, there will be multiple header with same name + -- which can also be valid local s: STRING_8 do @@ -509,6 +511,18 @@ feature -- Content related header put_header_key_value ({HTTP_HEADER_NAMES}.header_content_transfer_encoding, a_mechanism) end + put_content_language (a_lang: READABLE_STRING_8) + -- Put "Content-Language" header of value `a_lang'. + do + put_header_key_value ({HTTP_HEADER_NAMES}.header_content_language, a_lang) + end + + put_content_encoding (a_enc: READABLE_STRING_8) + -- Put "Content-Encoding" header of value `a_enc'. + do + put_header_key_value ({HTTP_HEADER_NAMES}.header_content_encoding, a_enc) + end + put_transfer_encoding (a_enc: READABLE_STRING_8) -- Put "Transfer-Encoding" header with for instance "chunked" do diff --git a/library/server/ewsgi/specification/request/wgi_meta_names.e b/library/server/ewsgi/specification/request/wgi_meta_names.e index a4578a6b..c4f9ee81 100644 --- a/library/server/ewsgi/specification/request/wgi_meta_names.e +++ b/library/server/ewsgi/specification/request/wgi_meta_names.e @@ -54,6 +54,18 @@ feature -- Access http_if_match: STRING = "HTTP_IF_MATCH" + http_if_modified_since: STRING = "HTTP_IF_MODIFIED_SINCE" + + http_if_none_match: STRING = "HTTP_IF_NONE_MATCH" + + http_if_range: STRING = "HTTP_IF_RANGE" + + http_if_unmodified_since: STRING = "HTTP_IF_UNMODIFIED_SINCE" + + http_last_modified: STRING = "HTTP_LAST_MODIFIED" + + http_range: STRING = "HTTP_RANGE" + gateway_interface: STRING = "GATEWAY_INTERFACE" auth_type: STRING = "AUTH_TYPE" diff --git a/library/server/ewsgi/specification/request/wgi_request.e b/library/server/ewsgi/specification/request/wgi_request.e index fb1f5ef0..697e0ba2 100644 --- a/library/server/ewsgi/specification/request/wgi_request.e +++ b/library/server/ewsgi/specification/request/wgi_request.e @@ -609,6 +609,36 @@ feature -- HTTP_* deferred end + http_if_modified_since: detachable READABLE_STRING_8 + -- Modification check on resource + deferred + end + + http_if_none_match: detachable READABLE_STRING_8 + -- Existence check on resource + deferred + end + + http_if_range: detachable READABLE_STRING_8 + -- Range check on resource + deferred + end + + http_if_unmodified_since: detachable READABLE_STRING_8 + -- Modification check on resource + deferred + end + + http_last_modified: detachable READABLE_STRING_8 + -- Modification time of resource + deferred + end + + http_range: detachable READABLE_STRING_8 + -- Requested byte-range of resource + deferred + end + feature -- Extra CGI environment variables request_uri: READABLE_STRING_8 diff --git a/library/server/ewsgi/src/implementation/wgi_request_from_table.e b/library/server/ewsgi/src/implementation/wgi_request_from_table.e index a7688281..4368bb33 100644 --- a/library/server/ewsgi/src/implementation/wgi_request_from_table.e +++ b/library/server/ewsgi/src/implementation/wgi_request_from_table.e @@ -253,6 +253,42 @@ feature -- Access: HTTP_* CGI meta parameters - 1.1 do Result := meta_string_variable ({WGI_META_NAMES}.http_if_match) end + + http_if_modified_since: detachable READABLE_STRING_8 + -- Modification check on resource + do + Result := meta_string_variable ({WGI_META_NAMES}.http_if_modified_since) + end + + http_if_none_match: detachable READABLE_STRING_8 + -- Existence check on resource + do + Result := meta_string_variable ({WGI_META_NAMES}.http_if_none_match) + end + + http_if_range: detachable READABLE_STRING_8 + -- Range check on resource + do + Result := meta_string_variable ({WGI_META_NAMES}.http_if_range) + end + + http_if_unmodified_since: detachable READABLE_STRING_8 + -- Modification check on resource + do + Result := meta_string_variable ({WGI_META_NAMES}.http_if_unmodified_since) + end + + http_last_modified: detachable READABLE_STRING_8 + -- Modification time of resource + do + Result := meta_string_variable ({WGI_META_NAMES}.http_last_modified) + end + + http_range: detachable READABLE_STRING_8 + -- Requested byte-range of resource + do + Result := meta_string_variable ({WGI_META_NAMES}.http_range) + end feature -- Access: Extension to CGI meta parameters - 1.1 diff --git a/library/server/wsf/router/wsf_caching_policy.e b/library/server/wsf/router/wsf_caching_policy.e new file mode 100644 index 00000000..35f104de --- /dev/null +++ b/library/server/wsf/router/wsf_caching_policy.e @@ -0,0 +1,137 @@ +note + + description: "[ + Policies for determing caching of responses. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class WSF_CACHING_POLICY + +feature -- Access + + Never_expires: NATURAL = 525600 + -- 525600 = 365 * 24 * 60 * 60 = (almost) 1 year; + -- See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21 for an explanation of why this means never expire + + max_age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale; + -- This is used to generate a Cache-Control: max-age header. + -- Return 0 to indicate already expired. + -- Return `Never_expires' to indicate never expires. + require + req_attached: req /= Void + deferred + ensure + not_more_than_1_year: Result <= Never_expires + end + + shared_age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale in a shared cache; + -- This is used to generate a Cache-Control: s-maxage header. + -- If you wish to have different expiry ages for shared and provate caches, redefine this routine. + -- Return 0 to indicate already expired. + -- Return `Never_expires' to indicate never expires. + require + req_attached: req /= Void + do + Result := max_age (req) + ensure + not_more_than_1_year: Result <= Never_expires + end + + http_1_0_age (req: WSF_REQUEST): NATURAL + -- Maximum age in seconds before response to `req` is considered stale; + -- This is used to generate an Expires header, which HTTP/1.0 caches understand. + -- If you wish to generate a different age for HTTP/1.0 caches, then redefine this routine. + -- Return 0 to indicate already expired. + -- Return `Never_expires' to indicate never expires. Note this will + -- make a result cachecable that would not normally be cacheable (such as as response + -- to a POST), unless overriden by cache-control headers, so be sure to check `req.request_method'. + require + req_attached: req /= Void + do + Result := max_age (req) + ensure + not_more_than_1_year: Result <= Never_expires + end + + is_freely_cacheable (req: WSF_REQUEST): BOOLEAN + -- Should the response to `req' be freely cachable in shared caches? + -- If `True', then a Cache-Control: public header will be generated. + require + req_attached: req /= Void + deferred + end + + is_transformable (req: WSF_REQUEST): BOOLEAN + -- Should a non-transparent proxy be allowed to modify headers of response to `req`? + -- The headers concerned are listed in http://www.w3.org/Protocols/rfc2616-sec14.html#sec14,9. + -- If `False' then a Cache-Control: no-transorm header will be generated. + require + req_attached: req /= Void + do + -- We choose a conservative default. But most applications can + -- redefine to return `True'. + end + + must_revalidate (req: WSF_REQUEST): BOOLEAN + -- If a client has requested, or a cache is configured, to ignore server's expiration time, + -- should we force revalidation anyway? + -- If `True' then a Cache-Control: must-revalidate header will be generated. + require + req_attached: req /= Void + do + -- Redefine to force revalidation. + end + + must_proxy_revalidate (req: WSF_REQUEST): BOOLEAN + -- If a shared cache is configured to ignore server's expiration time, + -- should we force revalidation anyway? + -- If `True' then a Cache-Control: proxy-revalidate header will be generated. + require + req_attached: req /= Void + do + -- Redefine to force revalidation. + end + + private_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8] + -- Header names intended for a single user. + -- If non-Void, then a Cache-Control: private header will be generated. + -- Returning an empty list prevents the entire response from being served from a shared cache. + require + req_attached: req /= Void + deferred + ensure + not_freely_cacheable: Result /= Void implies not is_freely_cacheable (req) + end + + non_cacheable_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8] + -- Header names that will not be sent from a cache without revalidation; + -- If non-Void, then a Cache-Control: no-cache header will be generated. + -- Returning an empty list prevents the response being served from a cache + -- without revalidation. + require + req_attached: req /= Void + deferred + end + + is_sensitive (req: WSF_REQUEST): BOOLEAN + -- Is the response to `req' of a sensitive nature? + -- If `True' then a Cache-Control: no-store header will be generated. + require + req_attached: req /= Void + deferred + 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/wsf_delete_helper.e b/library/server/wsf/router/wsf_delete_helper.e new file mode 100644 index 00000000..15f08204 --- /dev/null +++ b/library/server/wsf/router/wsf_delete_helper.e @@ -0,0 +1,66 @@ +note + + description: "[ + Policy-driven helpers to implement the DELETE method. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_DELETE_HELPER + +inherit + + WSF_METHOD_HELPER + +feature {NONE} -- Implementation + + send_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_new_resource: BOOLEAN) + -- Write response to deletion of resource named by `req' into `res'. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + local + l_dt: STRING + l_ok: BOOLEAN + do + a_handler.delete (req) + l_ok := a_handler.response_ok (req) + if l_ok then + if a_handler.includes_response_entity (req) then + a_handler.ensure_content_available (req) + l_ok := a_handler.response_ok (req) + if l_ok then + a_header.put_content_length (a_handler.content_length (req).as_integer_32) + end + -- we don't bother supporting chunked responses for DELETE. + else + a_header.put_content_length (0) + end + if attached req.request_time as l_time then + l_dt := (create {HTTP_DATE}.make_from_date_time (l_time)).rfc1123_string + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_date, l_dt) + generate_cache_headers (req, a_handler, a_header, l_time) + end + if a_handler.delete_queued (req) then + res.set_status_code ({HTTP_STATUS_CODE}.accepted) + res.put_header_text (a_header.string) + res.put_string (a_handler.content (req)) + elseif a_handler.deleted (req) then + if a_handler.includes_response_entity (req) then + res.set_status_code ({HTTP_STATUS_CODE}.ok) + res.put_header_text (a_header.string) + res.put_string (a_handler.content (req)) + else + res.set_status_code ({HTTP_STATUS_CODE}.no_content) + res.put_header_text (a_header.string) + end + end + end + if not l_ok then + write_error_response (req, res) + end + end + +end diff --git a/library/server/wsf/router/wsf_get_helper.e b/library/server/wsf/router/wsf_get_helper.e new file mode 100644 index 00000000..666518e2 --- /dev/null +++ b/library/server/wsf/router/wsf_get_helper.e @@ -0,0 +1,72 @@ +note + + description: "[ + Policy-driven helpers to implement processing of GET and HEAD requests. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_GET_HELPER + +inherit + + WSF_METHOD_HELPER + +feature {NONE} -- Implementation + + send_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_new_resource: BOOLEAN) + -- Write response to `req' into `res' in accordance with `a_media_type' etc. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + local + l_chunked, l_ok: BOOLEAN + l_dt: STRING + do + a_handler.ensure_content_available (req) + l_ok := a_handler.response_ok (req) + if l_ok then + l_chunked := a_handler.is_chunking (req) + if l_chunked then + a_header.put_transfer_encoding_chunked + else + a_header.put_content_length (a_handler.content_length (req).as_integer_32) + end + if attached req.request_time as l_time then + l_dt := (create {HTTP_DATE}.make_from_date_time (l_time)).rfc1123_string + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_date, l_dt) + generate_cache_headers (req, a_handler, a_header, l_time) + end + l_ok := a_handler.response_ok (req) + end + if l_ok then + res.set_status_code ({HTTP_STATUS_CODE}.ok) + else + write_error_response (req, res) + end + if attached a_handler.etag (req) as l_etag then + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_etag, l_etag) + end + res.put_header_text (a_header.string) + if l_ok then + if l_chunked then + send_chunked_response (req, res, a_handler, a_header) + else + res.put_string (a_handler.content (req)) + end + 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/router/wsf_method_helper.e b/library/server/wsf/router/wsf_method_helper.e new file mode 100644 index 00000000..3f3037e8 --- /dev/null +++ b/library/server/wsf/router/wsf_method_helper.e @@ -0,0 +1,598 @@ +note + + description: "[ + Policy-driven helpers to implement a method. + + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class WSF_METHOD_HELPER + +inherit + + HTTP_STATUS_CODE_MESSAGES + + SHARED_HTML_ENCODER + export {NONE} all end + +feature -- Access + + resource_exists: BOOLEAN + -- Does the requested resource (request URI) exist? + +feature -- Setting + + set_resource_exists + -- Set `resource_exists' to `True'. + do + resource_exists := True + ensure + set: resource_exists + end + +feature -- Basic operations + + execute_new_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to non-existing resource requested by `req.' into `res'. + -- Policy routines are available in `a_handler'. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + -- An HTTP_HEADER is also available as the execution variable "NEGOTIATED_HTTP_HEADER". + -- It includes the Vary header (if any) + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + local + l_locs: LIST [URI] + do + if a_handler.resource_previously_existed (req) then + if a_handler.resource_moved_permanently (req) then + l_locs := a_handler.previous_location (req) + handle_redirection_error (req, res, l_locs, {HTTP_STATUS_CODE}.moved_permanently) + elseif a_handler.resource_moved_temporarily (req) then + l_locs := a_handler.previous_location (req) + handle_redirection_error (req, res, l_locs, {HTTP_STATUS_CODE}.found) + else + check attached {HTTP_HEADER} req.execution_variable (a_handler.Negotiated_http_header_execution_variable) as h then + -- postcondition header_attached of `handle_content_negotiation' + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.gone) + res.put_header_lines (h) + end + end + else + check attached {HTTP_HEADER} req.execution_variable (a_handler.Negotiated_http_header_execution_variable) as h then + -- postcondition header_attached of `handle_content_negotiation' + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.not_found) + res.put_header_lines (h) + end + end + end + + execute_existing_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to existing resource requested by `req' into `res'. + -- Policy routines are available in `a_handler'. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + not_if_match_star: attached req.http_if_match as l_if_match implies not l_if_match.same_string ("*") + local + l_etags: LIST [READABLE_STRING_8] + l_failed: BOOLEAN + l_date: HTTP_DATE + do + if attached req.http_if_match as l_if_match then + -- also if-range when we add support for range requests + if not l_if_match.same_string ("*") then + l_etags := l_if_match.split (',') + l_failed := not across l_etags as i_etags some a_handler.matching_etag (req, i_etags.item, True) end + end + end + if l_failed then + handle_precondition_failed (req, res) + else + if attached req.http_if_unmodified_since as l_if_unmodified_since then + if l_if_unmodified_since.is_string_8 then + create l_date.make_from_string (l_if_unmodified_since.as_string_8) + if not l_date.has_error then + if a_handler.modified_since (req, l_date.date_time) then + handle_precondition_failed (req, res) + l_failed := True + end + end + end + end + if not l_failed then + if attached req.http_if_none_match as l_if_none_match then + l_etags := l_if_none_match.split (',') + l_failed := l_if_none_match.same_string ("*") or + across l_etags as i_etags some a_handler.matching_etag (req, i_etags.item, False) end + end + if l_failed then + handle_if_none_match_failed (req, res, a_handler) + else + if attached req.http_if_modified_since as l_if_modified_since then + if l_if_modified_since.is_string_8 then + create l_date.make_from_string (l_if_modified_since.as_string_8) + if not l_date.has_error then + if not a_handler.modified_since (req, l_date.date_time) then + handle_not_modified (req, res, a_handler) + l_failed := True + end + end + end + end + end + if not l_failed then + check attached {HTTP_HEADER} req.execution_variable (a_handler.Negotiated_http_header_execution_variable) as h then + -- postcondition header_attached of `handle_content_negotiation' + send_response (req, res, a_handler, h, False) + end + end + end + end + end + +feature -- Content negotiation + + handle_content_negotiation (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Negotiate acceptable content for, then write, response requested by `req' into `res'. + -- Policy routines are available in `a_handler'. + -- + -- Either a 406 Not Acceptable error is sent, or upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + -- An HTTP_HEADER is also saved as the execution variable "NEGOTIATED_HTTP_HEADER". + -- It includes the Vary header (if any) + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + local + l_conneg: CONNEG_SERVER_SIDE + h: HTTP_HEADER + l_media: MEDIA_TYPE_VARIANT_RESULTS + l_lang: LANGUAGE_VARIANT_RESULTS + l_charset: CHARACTER_ENCODING_VARIANT_RESULTS + l_encoding: COMPRESSION_VARIANT_RESULTS + l_mime_types, l_langs, l_charsets, l_encodings: LIST [STRING] + l_vary_star: BOOLEAN + do + create h.make + l_vary_star := not a_handler.predictable_response (req) + if l_vary_star then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, "*") + elseif attached a_handler.additional_variant_headers (req) as l_additional then + across l_additional as i_additional loop + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, i_additional.item) + end + end + l_conneg := a_handler.conneg (req) + l_mime_types := a_handler.mime_types_supported (req) + l_media := l_conneg.media_type_preference (l_mime_types, req.http_accept) + if not l_vary_star and l_mime_types.count > 1 and attached l_media.variant_header as l_media_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_media_variant) + end + if not l_media.is_acceptable then + handle_not_acceptable ("None of the requested ContentTypes were acceptable", l_mime_types, req, res) + else + l_langs := a_handler.languages_supported (req) + l_lang := l_conneg.language_preference (l_langs, req.http_accept_language) + if not l_vary_star and l_langs.count > 1 and attached l_lang.variant_header as l_lang_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_lang_variant) + end + if not l_lang.is_acceptable then + handle_not_acceptable ("None of the requested languages were acceptable", l_langs, req, res) + else + if attached l_lang.language_type as l_language_type then + h.put_content_language (l_language_type) + req.set_execution_variable (a_handler.Negotiated_language_execution_variable, l_language_type) + end + l_charsets := a_handler.charsets_supported (req) + l_charset := l_conneg.charset_preference (l_charsets, req.http_accept_charset) + if not l_vary_star and l_charsets.count > 1 and attached l_charset.variant_header as l_charset_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_charset_variant) + end + if not l_charset.is_acceptable then + handle_not_acceptable ("None of the requested character encodings were acceptable", l_charsets, req, res) + else + if attached l_media.media_type as l_media_type then + if attached l_charset.character_type as l_character_type then + h.put_content_type (l_media_type + "; charset=" + l_character_type) + req.set_execution_variable (a_handler.Negotiated_charset_execution_variable, l_charset) + else + h.put_content_type (l_media_type) + end + req.set_execution_variable (a_handler.Negotiated_media_type_execution_variable, l_media_type) + end + l_encodings := a_handler.encodings_supported (req) + l_encoding := l_conneg.encoding_preference (l_encodings, req.http_accept_encoding) + if not l_vary_star and l_encodings.count > 1 and attached l_encoding.variant_header as l_encoding_variant then + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_vary, l_encoding_variant) + end + if not l_encoding.is_acceptable then + handle_not_acceptable ("None of the requested transfer encodings were acceptable", l_encodings, req, res) + else + if attached l_encoding.compression_type as l_compression_type then + h.put_content_encoding (l_compression_type) + req.set_execution_variable (a_handler.Negotiated_encoding_execution_variable, l_compression_type) + end + end + end + end + end + req.set_execution_variable (a_handler.Negotiated_http_header_execution_variable, h) + ensure + header_attached: attached {HTTP_HEADER} req.execution_variable (a_handler.Negotiated_http_header_execution_variable) + end + +feature {NONE} -- Implementation + + send_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_new_resource: BOOLEAN) + -- Write response to `req' into `res'. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + a_header_attached: a_header /= Void + deferred + end + + send_chunked_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER) + -- Write response in chunks to `req'. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + local + l_chunk: TUPLE [a_chunk: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8] + do + from + if a_handler.response_ok (req) then + l_chunk := a_handler.next_chunk (req) + res.put_chunk (l_chunk.a_chunk, l_chunk.a_extension) + else + write_error_response (req, res) + end + until + a_handler.finished (req) or not a_handler.response_ok (req) + loop + a_handler.generate_next_chunk (req) + if a_handler.response_ok (req) then + l_chunk := a_handler.next_chunk (req) + res.put_chunk (l_chunk.a_chunk, l_chunk.a_extension) + else + write_error_response (req, res) + end + end + if a_handler.finished (req) then + -- In future, add support for trailers + res.put_chunk_end + end + end + + generate_cache_headers (req: WSF_REQUEST; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_request_time: DATE_TIME) + -- Write headers affecting caching for `req' into `a_header'. + require + req_attached: req /= Void + a_handler_attached: a_handler /= Void + a_header_attached: a_header /= Void + a_request_time_attached: a_request_time /= Void + local + l_age, l_age_1, l_age_2: NATURAL + l_dur: DATE_TIME_DURATION + l_dt, l_field_names: STRING + do + l_age := a_handler.http_1_0_age (req) + create l_dur.make (0, 0, 0, 0, 0, l_age.as_integer_32) + l_dt := (create {HTTP_DATE}.make_from_date_time (a_request_time + l_dur)).rfc1123_string + a_header.put_header_key_value ({HTTP_HEADER_NAMES}.header_expires, l_dt) + l_age_1 := a_handler.max_age (req) + if l_age_1 /= l_age then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "max-age=" + l_age_1.out) + end + l_age_2 := a_handler.shared_age (req) + if l_age_2 /= l_age_1 then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "s-maxage=" + l_age_2.out) + end + if a_handler.is_freely_cacheable (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "public") + elseif attached a_handler.private_headers (req) as l_fields then + l_field_names := "=" + if not l_fields.is_empty then + append_field_name (l_field_names, l_fields) + end + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "private" + l_field_names) + end + if attached a_handler.non_cacheable_headers (req) as l_fields then + l_field_names := "=" + if not l_fields.is_empty then + append_field_name (l_field_names, l_fields) + end + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "no-cache" + l_field_names) + end + if not a_handler.is_transformable (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "no-transform") + end + if a_handler.must_revalidate (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "must-revalidate") + end + if a_handler.must_proxy_revalidate (req) then + a_header.add_header_key_value ({HTTP_HEADER_NAMES}.header_cache_control, "proxy-revalidate") + end + end + + append_field_name (a_field_names: STRING; a_fields: LIST [READABLE_STRING_8]) + -- Append all of `a_fields' as a comma-separated list to `a_field_names'. + require + a_field_names_attached: a_field_names /= Void + a_fields_attached: a_fields /= Void + do + across a_fields as i_fields loop + a_field_names.append_string (i_fields.item) + if not i_fields.is_last then + a_field_names.append_character (',') + end + end + end + + +feature -- Error reporting + + write_error_response (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write an error response to `res' using `req.error_handler'. + require + req_attached: req /= Void + res_attached: res /= Void + req_has_error: req.has_error + local + h: HTTP_HEADER + m: READABLE_STRING_8 + do + m := req.error_handler.as_string_representation + create h.make + h.put_content_type_text_plain + h.put_content_length (m.count) + res.set_status_code (req.error_handler.primary_error_code) + res.put_header_lines (h) + res.put_string (m) + end + + handle_redirection_error (req: WSF_REQUEST; res: WSF_RESPONSE; a_locations: LIST [URI]; a_status_code: INTEGER) + -- Write `a_status_code' error to `res'. + -- Include all of `a_locations' in the headers, and hyperlink to the first one in the body. + require + res_attached: res /= Void + req_attached: req /= Void + a_locations_attached: a_locations /= Void + a_location_not_empty: not a_locations.is_empty + a_status_code_code: is_valid_http_status_code (a_status_code) + local + h: HTTP_HEADER + s: STRING + do + if attached http_status_code_message (a_status_code) as l_msg then + create h.make + across a_locations as i_location loop + h.add_header_key_value ({HTTP_HEADER_NAMES}.header_location, i_location.item.string) + end + if req.is_content_type_accepted ({HTTP_MIME_TYPES}.text_html) then + s := "" + s.append ("") + s.append (html_encoder.encoded_string (req.request_uri)) + s.append ("Error " + a_status_code.out + " (" + l_msg + ")") + s.append ("%N") + s.append ("[ + + + + ]") + s.append ("
Error " + a_status_code.out + " (" + l_msg + ")
") + s.append ("
") + s.append ("
") + s.append ("
") + s.append ("
") + s.append ("
") + s.append ("The current location for this resource is here") + s.append ("Error " + a_status_code.out + " (" + l_msg + ")
") + s.append ("
Error " + a_status_code.out + " (" + l_msg + "): " + html_encoder.encoded_string (req.request_uri) + "
") + s.append ("
") + s.append ("%N") + s.append ("%N") + + h.put_content_type_text_html + else + s := "Error " + a_status_code.out + " (" + l_msg + "): " + s.append (req.request_uri) + s.append_character ('%N') + s.append ("The current location for this resource is " + a_locations.first.string) + h.put_content_type_text_plain + end + h.put_content_length (s.count) + res.put_header_lines (h) + res.put_string (s) + res.flush + end + end + + handle_not_acceptable (a_message: READABLE_STRING_8; a_supported: LIST [STRING]; req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Not Acceptable response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + a_message_attached: a_message /= Void + a_supported_attached: a_supported /= Void + local + h: HTTP_HEADER + s: STRING + do + create h.make + h.put_content_type_text_plain + h.put_current_date + s := a_message + s.append_character ('%N') + s.append_character ('%N') + s.append_string ("We accept the following:%N%N") + across a_supported as i_supported loop + s.append_string (i_supported.item) + s.append_character ('%N') + end + h.put_content_length (s.count) + res.set_status_code ({HTTP_STATUS_CODE}.not_acceptable) + res.put_header_lines (h) + res.put_string (s) + res.flush + end + + handle_if_none_match_failed (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write a Not Modified response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + do + handle_not_modified (req, res, a_handler) + end + + handle_not_modified (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write a Not Modified response to `res'. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + local + h: HTTP_HEADER + do + create h.make + h.put_content_type_text_plain + h.put_content_length (0) + if attached a_handler.etag (req) as l_etag then + h.put_header_key_value ({HTTP_HEADER_NAMES}.header_etag, l_etag) + end + generate_cache_headers (req, a_handler, h, create {DATE_TIME}.make_now_utc) + res.set_status_code ({HTTP_STATUS_CODE}.not_modified) + res.put_header_lines (h) + end + + handle_precondition_failed (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Precondition Failed response for `req' to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + local + h: HTTP_HEADER + do + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.precondition_failed) + res.put_header_lines (h) + end + + handle_unsupported_media_type (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Unsupported Media Type response for `req' to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + local + h: HTTP_HEADER + do + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.unsupported_media_type) + res.put_header_lines (h) + end + + handle_not_implemented (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Write a Not Implemented response for `req' to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + local + h: HTTP_HEADER + do + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.not_implemented) + res.put_header_lines (h) + end + + handle_request_entity_too_large (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write a Request Entity Too Large response for `req' to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + a_handler_attached: a_handler /= Void + local + h: HTTP_HEADER + do + create h.make + h.put_content_type_text_plain + h.put_current_date + h.put_content_length (0) + res.set_status_code ({HTTP_STATUS_CODE}.request_entity_too_large) + res.put_header_lines (h) + -- FIXME: Need to check if condition is temporary. This needs a new query + -- on the handler. For now we can claim compliance by saying the condition + -- is always permenent :-) - author's might not like this though. + 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/wsf_method_helper_factory.e b/library/server/wsf/router/wsf_method_helper_factory.e new file mode 100644 index 00000000..633599b6 --- /dev/null +++ b/library/server/wsf/router/wsf_method_helper_factory.e @@ -0,0 +1,42 @@ +note + + description: "[ + Default factory for policy-driven method helpers. + Extension methods can be implemented here. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_METHOD_HELPER_FACTORY + +feature -- Factory + + new_method_helper (a_method: READABLE_STRING_8): detachable WSF_METHOD_HELPER + -- New object for processing `a_method'; + -- Redefine this routine to implement extension methods. + require + a_method_attached: a_method /= Void + do + if a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_get) or + a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_head) then + create {WSF_GET_HELPER} Result + elseif a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_put) then + create {WSF_PUT_HELPER} Result + elseif a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_post) then + create {WSF_POST_HELPER} Result + elseif a_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_delete) then + create {WSF_DELETE_HELPER} Result + 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/router/wsf_options_policy.e b/library/server/wsf/router/wsf_options_policy.e new file mode 100644 index 00000000..418ea12b --- /dev/null +++ b/library/server/wsf/router/wsf_options_policy.e @@ -0,0 +1,37 @@ +note + + description: "[ + Default policy for responing to OPTIONS requests other than OPTIONS* + By overriding `execute_options', clients can add a body, for example. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_OPTIONS_POLICY + +feature -- Basic operations + + execute_options (req: WSF_REQUEST; res: WSF_RESPONSE; a_router: WSF_ROUTER) + -- Write response to `req' into `res'. + require + req_attached: req /= Void + options_request: req.is_request_method ({HTTP_REQUEST_METHODS}.method_options) + res_attached: res /= Void + a_router_attached: a_router /= Void + local + l_methods: WSF_REQUEST_METHODS + h: HTTP_HEADER + do + create h.make + res.set_status_code ({HTTP_STATUS_CODE}.ok) + h.put_content_type ({HTTP_MIME_TYPES}.text_plain) + h.put_current_date + h.put_content_length (0) + l_methods := a_router.allowed_methods_for_request (req) + if not l_methods.is_empty then + h.put_allow (l_methods) + end + res.put_header_text (h.string) + end + +end diff --git a/library/server/wsf/router/wsf_post_helper.e b/library/server/wsf/router/wsf_post_helper.e new file mode 100644 index 00000000..96ae57a4 --- /dev/null +++ b/library/server/wsf/router/wsf_post_helper.e @@ -0,0 +1,80 @@ +note + + description: "[ + Policy-driven helpers to implement POST. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_POST_HELPER + +inherit + + WSF_METHOD_HELPER + redefine + execute_new_resource + end + +feature -- Basic operations + + execute_new_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to non-existing resource requested by `req.' into `res'. + -- Policy routines are available in `a_handler'. + do + if a_handler.allow_post_to_missing_resource (req) then + check attached {HTTP_HEADER} req.execution_variable (a_handler.Negotiated_http_header_execution_variable) as h then + -- postcondition header_attached of `handle_content_negotiation' + send_response (req, res, a_handler, h, True) + end + else + res.send (create {WSF_NOT_FOUND_RESPONSE}.make(req)) + end + end + + +feature {NONE} -- Implementation + + send_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_new_resource: BOOLEAN) + -- Write response to `req' into `res' in accordance with `a_media_type' etc. as a new URI. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + local + l_code: NATURAL + do + a_handler.read_entity (req) + if a_handler.is_entity_too_large (req) then + handle_request_entity_too_large (req, res, a_handler) + else + a_handler.check_content_headers (req) + l_code := a_handler.content_check_code (req) + if l_code /= 0 then + if l_code = 415 then + handle_unsupported_media_type (req, res) + else + handle_not_implemented (req, res) + end + else + a_handler.check_request (req, res) + if a_handler.request_check_code (req) = 0 then + a_handler.append_resource (req, res) + -- 200 or 204 or 303 or 500 (add support for this?) + -- FIXME: more support, such as includes_response_entity + end + end + 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/router/wsf_previous_policy.e b/library/server/wsf/router/wsf_previous_policy.e new file mode 100644 index 00000000..c77f0746 --- /dev/null +++ b/library/server/wsf/router/wsf_previous_policy.e @@ -0,0 +1,63 @@ +note + + description: "[ + Policies for deciding if a resource that currently doesn't exist used to do so. + This default implementation assumes that no resources used to exist. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class WSF_PREVIOUS_POLICY + +feature -- Access + + resource_previously_existed (req: WSF_REQUEST): BOOLEAN + -- Did `req.path_translated' exist previously? + require + req_attached: req /= Void + do + -- No. Override if this is not want you want. + end + + resource_moved_permanently (req: WSF_REQUEST): BOOLEAN + -- Was `req.path_translated' moved permanently? + require + req_attached: req /= Void + previously_existed: resource_previously_existed (req) + do + -- No. Override if this is not want you want. + end + + resource_moved_temporarily (req: WSF_REQUEST): BOOLEAN + -- Was `req.path_translated' moved temporarily? + require + req_attached: req /= Void + previously_existed: resource_previously_existed (req) + do + -- No. Override if this is not want you want. + end + + previous_location (req: WSF_REQUEST): LIST [URI] + -- Previous location(s) for resource named by `req'; + require + req_attached: req /= Void + previously_existed: resource_previously_existed (req) + moved: resource_moved_permanently (req) or resource_moved_temporarily (req) + do + create {LINKED_LIST [URI]} Result.make + ensure + previous_location_attached: Result /= Void + non_empty_list: not Result.is_empty + 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/wsf_put_helper.e b/library/server/wsf/router/wsf_put_helper.e new file mode 100644 index 00000000..41095b67 --- /dev/null +++ b/library/server/wsf/router/wsf_put_helper.e @@ -0,0 +1,82 @@ +note + + description: "[ + Policy-driven helpers to implement PUT. + ]" + date: "$Date$" + revision: "$Revision$" + +class WSF_PUT_HELPER + +inherit + + WSF_METHOD_HELPER + redefine + execute_new_resource + end + +feature -- Basic operations + + execute_new_resource (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER) + -- Write response to non-existing resource requested by `req.' into `res'. + -- Policy routines are available in `a_handler'. + do + if a_handler.treat_as_moved_permanently (req) then + handle_redirection_error (req, res, a_handler.previous_location (req), {HTTP_STATUS_CODE}.moved_permanently) + else + check attached {HTTP_HEADER} req.execution_variable (a_handler.Negotiated_http_header_execution_variable) as h then + -- postcondition header_attached of `handle_content_negotiation' + send_response (req, res, a_handler, h, True) + end + end + end + +feature {NONE} -- Implementation + + send_response (req: WSF_REQUEST; res: WSF_RESPONSE; a_handler: WSF_SKELETON_HANDLER; a_header: HTTP_HEADER; a_new_resource: BOOLEAN) + -- Write response to `req' into `res' in accordance with `a_media_type' etc. as a new URI. + local + l_code: NATURAL + do + a_handler.read_entity (req) + if a_handler.is_entity_too_large (req) then + handle_request_entity_too_large (req, res, a_handler) + else + a_handler.check_content_headers (req) + l_code := a_handler.content_check_code (req) + if l_code /= 0 then + if l_code = 415 then + handle_unsupported_media_type (req, res) + else + handle_not_implemented (req, res) + end + else + a_handler.check_request (req, res) + if a_handler.request_check_code (req) = 0 then + if a_new_resource then + a_handler.create_resource (req, res) + -- 201 or 500 (add support for this?) + else + a_handler.check_conflict (req, res) + if a_handler.conflict_check_code (req) = 0 then + a_handler.update_resource (req, res) + -- 204 or 500 (add support for this?) + -- FIXME: more support, such as includes_response_entity + end + end + end + end + 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/router/wsf_skeleton_handler.e b/library/server/wsf/router/wsf_skeleton_handler.e new file mode 100644 index 00000000..60ebf617 --- /dev/null +++ b/library/server/wsf/router/wsf_skeleton_handler.e @@ -0,0 +1,550 @@ +note + + description: "[ + Policy-driven handlers. + Implementers only need to concentrate on creating content. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class WSF_SKELETON_HANDLER + +inherit + + WSF_URI_TEMPLATE_HANDLER + redefine + execute + end + + WSF_OPTIONS_POLICY + + WSF_PREVIOUS_POLICY + + WSF_CACHING_POLICY + + WSF_METHOD_HELPER_FACTORY + + WSF_SELF_DOCUMENTED_HANDLER + +feature {NONE} -- Initialization + + make_with_router (a_router: WSF_ROUTER) + -- Initialize `router'. + require + a_router_attached: a_router /= Void + do + router := a_router + ensure + router_aliased: router = a_router + end + +feature -- Router + + router: WSF_ROUTER + -- So that WSF_OPTIONS_POLICY can find the allowed methods + +feature -- Execution variables + + Negotiated_language_execution_variable: STRING = "NEGOTIATED_LANGUAGE" + -- Execution variable set by framework + + Negotiated_charset_execution_variable: STRING = "NEGOTIATED_CHARSET" + -- Execution variable set by framework + + Negotiated_media_type_execution_variable: STRING = "NEGOTIATED_MEDIA_TYPE" + -- Execution variable set by framework + + Negotiated_encoding_execution_variable: STRING = "NEGOTIATED_ENCODING" + -- Execution variable set by framework + + Negotiated_http_header_execution_variable: STRING = "NEGOTIATED_HTTP_HEADER" + -- Execution variable set by framework + + Request_entity_execution_variable: STRING = "REQUEST_ENTITY" + -- Execution variable set by framework + + Conflict_check_code_execution_variable: STRING = "CONFLICT_CHECK_CODE" + -- Execution variable set by framework + + Content_check_code_execution_variable: STRING = "CONTENT_CHECK_CODE" + -- Execution variable set by framework + + Request_check_code_execution_variable: STRING = "REQUEST_CHECK_CODE" + -- Execution variable set by framework + +feature -- Access + + is_chunking (req: WSF_REQUEST): BOOLEAN + -- Will the response to `req' using chunked transfer encoding? + require + req_attached: req /= Void + deferred + end + + includes_response_entity (req: WSF_REQUEST): BOOLEAN + -- Does the response to `req' include an entity? + -- Method will be DELETE, OUT, POST or an extension method. + require + req_attached: req /= Void + deferred + end + + conneg (req: WSF_REQUEST): CONNEG_SERVER_SIDE + -- Content negotiation for `req'; + -- This would normally be a once object, ignoring `req'. + require + req_attached: req /= Void + deferred + end + + mime_types_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + mime_types_supported_includes_default: Result.has (conneg (req).mime_default) + end + + languages_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Language header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + languages_supported_includes_default: Result.has (conneg (req).language_default) + end + + charsets_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Charset header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + charsets_supported_includes_default: Result.has (conneg (req).charset_default) + end + + encodings_supported (req: WSF_REQUEST): LIST [STRING] + -- All values for Accept-Encoding header that `Current' can serve + require + req_attached: req /= Void + deferred + ensure + encodings_supported_includes_default: Result.has (conneg (req).encoding_default) + end + + additional_variant_headers (req: WSF_REQUEST): detachable LIST [STRING] + -- Header other than Accept, Accept-Language, Accept-Charset and Accept-Encoding, + -- which might affect the response + do + end + + predictable_response (req: WSF_REQUEST): BOOLEAN + -- Does the response to `req' vary only on the dimensions of ContentType, Language, Charset and Transfer encoding, + -- plus those named in `additional_variant_headers'? + do + Result := True + -- redefine to return `False', so as to induce a Vary: * header + end + + matching_etag (req: WSF_REQUEST; a_etag: READABLE_STRING_32; a_strong: BOOLEAN): BOOLEAN + -- Is `a_etag' a match for resource requested in `req'? + -- If `a_strong' then the strong comparison function must be used. + require + req_attached: req /= Void + deferred + end + + etag (req: WSF_REQUEST): detachable READABLE_STRING_8 + -- Optional Etag for response entity to `req'; + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + deferred + end + + modified_since (req: WSF_REQUEST; a_date_time: DATE_TIME): BOOLEAN + -- Has resource requested in `req' been modified since `a_date_time' (UTC)? + require + req_attached: req /= Void + deferred + end + + treat_as_moved_permanently (req: WSF_REQUEST): BOOLEAN + -- Rather than store as a new entity, do we treat it as an existing entity that has been moved? + require + req_attached: req /= Void + put_request: req.is_request_method ({HTTP_REQUEST_METHODS}.method_put) + do + -- No. Redefine this if needed. + end + + allow_post_to_missing_resource (req: WSF_REQUEST): BOOLEAN + -- The resource named in `req' does not exist, and this is a POST. Do we allow it? + require + req_attached: req /= Void + deferred + end + +feature -- Measurement + + content_length (req: WSF_REQUEST): NATURAL + -- Length of entity-body of the response to `req' + require + req_attached: req /= Void + not_chunked: not is_chunking (req) + deferred + end + +feature -- Status report + + finished (req: WSF_REQUEST): BOOLEAN + -- Has the last chunk been generated for `req'? + require + req_attached: req /= Void + chunked: is_chunking (req) + deferred + end + + +feature -- Documentation + + mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION + -- Documentation associated with Current handler, in the context of the mapping `m' and methods `a_request_methods' + --| `m' and `a_request_methods' are useful to produce specific documentation when the handler is used for multiple mapping. + do + create Result.make (m) + Result.add_description (description) + end + + description: READABLE_STRING_GENERAL + -- General description for self-generated documentation; + -- The specific URI templates supported will be described automatically + deferred + ensure + description_attached: Result /= Void + end + +feature -- DELETE + + delete (req: WSF_REQUEST) + -- Delete resource named in `req' or set an error on `req.error_handler'. + require + req_attached: req /= Void + deferred + ensure + error_or_queued_or_deleted: not req.error_handler.has_error implies (delete_queued (req) or deleted (req)) + end + + deleted (req: WSF_REQUEST): BOOLEAN + -- Has resource named by `req' been deleted? + require + req_attached: req /= Void + do + if not req.error_handler.has_error then + Result := True + end + ensure + negative_implication: not Result implies req.error_handler.has_error + end + + delete_queued (req: WSF_REQUEST): BOOLEAN + -- Has resource named by `req' been queued for deletion? + require + req_attached: req /= Void + deferred + ensure + entity_available: includes_response_entity (req) + end + +feature -- GET/HEAD content + + ensure_content_available (req: WSF_REQUEST) + -- Commence generation of response text (entity-body) (if not already done in `check_resource_exists'). + -- If not chunked, then this will create the entire entity-body so as to be available + -- for a subsequent call to `content'. + -- If chunked, only the first chunk will be made available to `next_chunk'. If chunk extensions + -- are used, then this will also generate the chunk extension for the first chunk. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + -- If you support etags, and you have more than one possible representation + -- for the resource (so that your etag depends upon the particular representation), + -- then you will probably have already created the response entity in `check_resource_exists'. + require + req_attached: req /= Void + get_or_head_or_delete: req.is_get_head_request_method or req.is_delete_request_method + deferred + end + + response_ok (req: WSF_REQUEST): BOOLEAN + -- Has generation of the response (so-far, if chunked) proceeded witout error? + require + req_attached: req /= Void + do + Result := not req.error_handler.has_error + ensure + last_error_set: Result = not req.error_handler.has_error + end + + content (req: WSF_REQUEST): READABLE_STRING_8 + -- Non-chunked entity body in response to `req'; + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + head_get_or_delete: req.is_get_head_request_method or req.is_delete_request_method + no_error: response_ok (req) + not_chunked: not is_chunking (req) + deferred + end + + generate_next_chunk (req: WSF_REQUEST) + -- Prepare next chunk (including optional chunk extension) of entity body in response to `req'. + -- This is not called for the first chunk. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + no_error: response_ok (req) + chunked: is_chunking (req) + deferred + end + + next_chunk (req: WSF_REQUEST): TUPLE [a_check: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8] + -- Next chunk of entity body in response to `req'; + -- The second field of the result is an optional chunk extension. + -- Four execution variables are set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + require + req_attached: req /= Void + no_error: response_ok (req) + chunked: is_chunking (req) + deferred + end + +feature -- PUT/POST + + read_entity (req: WSF_REQUEST) + -- Read request body and set as `req.execution_variable (Request_entity_execution_variable)'. + require + req_attached: req /= Void + local + l_body: STRING + do + create l_body.make_empty + req.read_input_data_into (l_body) + if not l_body.is_empty then + req.set_execution_variable (Request_entity_execution_variable, l_body) + end + end + + is_entity_too_large (req: WSF_REQUEST): BOOLEAN + -- Is the entity stored in `req.execution_variable (Request_entity_execution_variable)' too large for the application? + require + req_attached: req /= Void + deferred + end + + check_content_headers (req: WSF_REQUEST) + -- Check we can support all content headers on request entity. + -- Set `req.execution_variable (Content_check_code_execution_variable)' to {NATURAL} zero if OK, or 415 or 501 if not. + require + req_attached: req /= Void + deferred + end + + content_check_code (req: WSF_REQUEST): NATURAL + -- Code set by `check_content_headers'. + require + req_attached: req /= Void + do + if attached {NATURAL} req.execution_variable (Content_check_code_execution_variable) as l_code then + Result := l_code + end + end + + create_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Create new resource in response to a PUT request when `check_resource_exists' returns `False'. + -- Implementor must set error code of 200 OK or 500 Server Error. + require + req_attached: req /= Void + res_attached: res /= Void + put_request: req.is_put_request_method + deferred + end + + append_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Create new resource in response to a POST request. + -- Implementor must set error code of 200 OK or 204 No Content or 303 See Other or 500 Server Error. + require + req_attached: req /= Void + res_attached: res /= Void + post_request: req.is_post_request_method + deferred + end + + check_conflict (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Check to see if updating the resource is problematic due to the current state of the resource. + -- Set `req.execution_variable (Conflict_check_code_execution_variable)' to {NATURAL} zero if OK, or 409 if not. + -- In the latter case, write the full error response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + deferred + end + + conflict_check_code (req: WSF_REQUEST): NATURAL + -- Code set by `check_conflict'. + require + req_attached: req /= Void + do + if attached {NATURAL} req.execution_variable (Conflict_check_code_execution_variable) as l_code then + Result := l_code + end + end + + check_request (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Check that the request entity is a valid request. + -- The entity is available as `req.execution_variable (Conflict_check_code_execution_variable)'. + -- Set `req.execution_variable (Request_check_code_execution_variable)' to {NATURAL} zero if OK, or 400 if not. + -- In the latter case, write the full error response to `res'. + require + req_attached: req /= Void + res_attached: res /= Void + put_or_post: req.is_put_post_request_method + deferred + end + + request_check_code (req: WSF_REQUEST): NATURAL + -- Code set by `check_request'. + require + req_attached: req /= Void + do + if attached {NATURAL} req.execution_variable (Request_check_code_execution_variable) as l_code then + Result := l_code + end + end + + update_resource (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Perform the update requested in `req'. + -- Write a response to `res' with a code of 204 or 500. + require + req_attached: req /= Void + res_attached: res /= Void + put_request: req.is_put_request_method + deferred + end + +feature -- Execution + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- + do + check + known_method: router.allowed_methods_for_request (req).has (req.request_method) + not_trace: not req.is_request_method ({HTTP_REQUEST_METHODS}.method_trace) + not_connect: not req.is_request_method ({HTTP_REQUEST_METHODS}.method_connect) + end + if req.is_request_method ({HTTP_REQUEST_METHODS}.method_options) then + execute_options (req, res, router) + else + if attached new_method_helper (req.request_method) as l_helper then + execute_method (req, res, l_helper) + else + handle_internal_server_error (res) + end + end + end + + execute_method (req: WSF_REQUEST; res: WSF_RESPONSE; a_helper: WSF_METHOD_HELPER) + -- Write response to `req' into `res', using `a_helper' as a logic helper. + require + req_attached: req /= Void + res_attached: res /= Void + a_helper_attached: a_helper /= Void + do + a_helper.handle_content_negotiation (req, res, Current) + if not res.status_is_set or else res.status_code /= {HTTP_STATUS_CODE}.Not_acceptable then + check_resource_exists (req, a_helper) + if a_helper.resource_exists then + a_helper.execute_existing_resource (req, res, Current) + else + if attached req.http_if_match as l_if_match and then l_if_match.same_string ("*") then + a_helper.handle_precondition_failed (req, res) + else + a_helper.execute_new_resource (req, res, Current) + end + end + end + end + + check_resource_exists (req: WSF_REQUEST; a_helper: WSF_METHOD_HELPER) + -- Call `a_helper.set_resource_exists' to indicate that `req.path_translated' + -- is the name of an existing resource. + -- Upto four execution variables may be set on `req': + -- "NEGOTIATED_MEDIA_TYPE" + -- "NEGOTIATED_LANGUAGE" + -- "NEGOTIATED_CHARSET" + -- "NEGOTIATED_ENCODING" + -- If you support etags, and you have more than one possible representation + -- for the resource (so that your etag depends upon the particular representation), + -- then you will probably need to create the response entity at this point, rather + -- than in `ensure_content_available'. + require + req_attached: req /= Void + a_helper_attached: a_helper /= Void + deferred + end + +feature {NONE} -- Implementation + + handle_internal_server_error (res: WSF_RESPONSE) + -- Write "Internal Server Error" response to `res'. + require + res_attached: res /= Void + local + h: HTTP_HEADER + m: STRING_8 + do + create h.make + h.put_content_type_text_plain + m := "Server failed to handle request properly" + h.put_content_length (m.count) + h.put_current_date + res.set_status_code ({HTTP_STATUS_CODE}.internal_server_error) + res.put_header_lines (h) + res.put_string (m) + ensure + response_status_is_set: res.status_is_set + status_is_service_unavailable: res.status_code = {HTTP_STATUS_CODE}.internal_server_error + body_sent: res.message_committed and then res.transfered_content_length > 0 + 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_request.e b/library/server/wsf/src/wsf_request.e index 36a38f19..677f2a24 100644 --- a/library/server/wsf/src/wsf_request.e +++ b/library/server/wsf/src/wsf_request.e @@ -309,18 +309,44 @@ feature -- Helper Result := request_method.is_case_insensitive_equal (m.as_string_8) end + is_put_request_method: BOOLEAN + -- Is Current a PUT request method? + do + Result := request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_put) + end + is_post_request_method: BOOLEAN -- Is Current a POST request method? do Result := request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_post) end + is_delete_request_method: BOOLEAN + -- Is Current a DELETE request method? + do + Result := request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_delete) + end + is_get_request_method: BOOLEAN -- Is Current a GET request method? do Result := request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_get) end + is_get_head_request_method: BOOLEAN + -- Is Current a GET or a HEAD request method? + do + Result := request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_get) or + request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_head) + end + + is_put_post_request_method: BOOLEAN + -- Is Current a PUT or a POST request method? + do + Result := request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_put) or + request_method.is_case_insensitive_equal ({HTTP_REQUEST_METHODS}.method_post) + end + is_content_type_accepted (a_content_type: READABLE_STRING_GENERAL): BOOLEAN -- Does client accepts content_type for the response? --| Based on header "Accept:" that can be for instance @@ -348,6 +374,8 @@ feature -- Helper end end Result := l_accept.has_substring (a_content_type) + else + Result := True end end @@ -1137,6 +1165,42 @@ feature -- HTTP_* Result := wgi_request.http_if_match end + http_if_modified_since: detachable READABLE_STRING_8 + -- Modification check on resource + do + Result := wgi_request.http_if_modified_since + end + + http_if_none_match: detachable READABLE_STRING_8 + -- Existence check on resource + do + Result := wgi_request.http_if_none_match + end + + http_if_range: detachable READABLE_STRING_8 + -- Range check on resource + do + Result := wgi_request.http_if_range + end + + http_if_unmodified_since: detachable READABLE_STRING_8 + -- Modification check on resource + do + Result := wgi_request.http_if_unmodified_since + end + + http_last_modified: detachable READABLE_STRING_8 + -- Modification time of resource + do + Result := wgi_request.http_last_modified + end + + http_range: detachable READABLE_STRING_8 + -- Requested byte-range of resource + do + Result := wgi_request.http_range + end + feature -- Extra CGI environment variables request_uri: READABLE_STRING_8 diff --git a/library/server/wsf/wsf-safe.ecf b/library/server/wsf/wsf-safe.ecf index 06b6121b..e47d827a 100644 --- a/library/server/wsf/wsf-safe.ecf +++ b/library/server/wsf/wsf-safe.ecf @@ -18,6 +18,7 @@ +
diff --git a/library/server/wsf/wsf.ecf b/library/server/wsf/wsf.ecf index 3d19ec4d..b315cfb2 100644 --- a/library/server/wsf/wsf.ecf +++ b/library/server/wsf/wsf.ecf @@ -15,7 +15,9 @@ - + + diff --git a/library/utility/general/error/src/error_handler.e b/library/utility/general/error/src/error_handler.e index e6ff72fa..f112a0c5 100644 --- a/library/utility/general/error/src/error_handler.e +++ b/library/utility/general/error/src/error_handler.e @@ -25,6 +25,16 @@ feature {NONE} -- Initialization create error_added_actions end +feature -- Access + + primary_error_code: INTEGER + -- Code of first error in `errors' + require + at_least_one_error: has_error + do + Result := errors.first.code + end + feature -- Status has_error: BOOLEAN