From 980dd7dfa8c93344fc9fdbeab51132dab43ff804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Fri, 19 Jul 2024 18:13:33 +0200 Subject: [PATCH] adapter.http: remove trailing slashes from routes This removes trailing slashes (and redirects to paths with trailing slashes) from the API and makes it compatible with the PCF2 showcase and other webapps. Previously, all routes were implemented with a trailing slash, e.g. `/submodels/` instead of `/submodels`. While the API spec only specifies the routes without a trailing slash, this has the advantage of being compatible with requests to the path with a trailing slash and without trailing slash, as werkzeug redirects requests to the slash-terminated path, if available. However, this poses a problem with browsers that make use of [CORS preflight requests][1] (e.g. Chromium-based browsers). Here, before doing an actual API request, the browser sends an `OPTIONS` request to the path it wants to request. This is done to check potential CORS headers (e.g. `Access-Control-Allow-Origin`) for the path, without retrieving the actual data. Our implementation doesn't support `OPTIONS` requests, which is fine. After the browser has received the response to the preflight request (which may or may not have been successful), it attempts to retrieve the actual data by sending the request again with the correct request method (e.g. `GET`). With our server this request now results in a redirect, as we redirect to the path with a trailing slash appended. This is a problem, as the browser didn't send a CORS preflight request to the path it is now redirected to. It also doesn't attempt to send another CORS preflight request, as it already sent one, with the difference being the now slash-terminated path. Thus, following the redirect is prevented by CORS policy and the data fails to load. By making the routes available via non-slash-terminated paths we avoid the need for redirects, which makes the server compatible with webapps viewed in browsers that use preflight requests. Requests to slash-terminated paths will no longer work (they won't redirect to the path without trailing slash). This shouldn't be a problem though, as the API is only specified without trailing slashes anyway. --- basyx/aas/adapter/http.py | 198 ++++++++++++++++++-------------------- 1 file changed, 91 insertions(+), 107 deletions(-) diff --git a/basyx/aas/adapter/http.py b/basyx/aas/adapter/http.py index 6cbf7fc53..43c1f985f 100644 --- a/basyx/aas/adapter/http.py +++ b/basyx/aas/adapter/http.py @@ -401,139 +401,123 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs self.file_store: aasx.AbstractSupplementaryFileContainer = file_store self.url_map = werkzeug.routing.Map([ Submount(base_path, [ - Submount("/serialization", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented) - ]), - Submount("/description", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented) - ]), + Rule("/serialization", methods=["GET"], endpoint=self.not_implemented), + Rule("/description", methods=["GET"], endpoint=self.not_implemented), + Rule("/shells", methods=["GET"], endpoint=self.get_aas_all), + Rule("/shells", methods=["POST"], endpoint=self.post_aas), Submount("/shells", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_all), - Rule("/", methods=["POST"], endpoint=self.post_aas), - Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_all_reference), + Rule("/$reference", methods=["GET"], endpoint=self.get_aas_all_reference), + Rule("/", methods=["GET"], endpoint=self.get_aas), + Rule("/", methods=["PUT"], endpoint=self.put_aas), + Rule("/", methods=["DELETE"], endpoint=self.delete_aas), Submount("/", [ - Rule("/", methods=["GET"], endpoint=self.get_aas), - Rule("/", methods=["PUT"], endpoint=self.put_aas), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas), - Rule("/$reference/", methods=["GET"], endpoint=self.get_aas_reference), - Submount("/asset-information", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_asset_information), - Rule("/", methods=["PUT"], endpoint=self.put_aas_asset_information), - Submount("/thumbnail", [ - Rule("/", methods=["GET"], endpoint=self.not_implemented), - Rule("/", methods=["PUT"], endpoint=self.not_implemented), - Rule("/", methods=["DELETE"], endpoint=self.not_implemented) - ]) - ]), - Submount("/submodel-refs", [ - Rule("/", methods=["GET"], endpoint=self.get_aas_submodel_refs), - Rule("/", methods=["POST"], endpoint=self.post_aas_submodel_refs), - Rule("//", methods=["DELETE"], - endpoint=self.delete_aas_submodel_refs_specific) - ]), - Submount("/submodels/", [ - Rule("/", methods=["PUT"], endpoint=self.put_aas_submodel_refs_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_aas_submodel_refs_submodel), - Rule("/", endpoint=self.aas_submodel_refs_redirect), - Rule("//", endpoint=self.aas_submodel_refs_redirect) + Rule("/$reference", methods=["GET"], endpoint=self.get_aas_reference), + Rule("/asset-information", methods=["GET"], endpoint=self.get_aas_asset_information), + Rule("/asset-information", methods=["PUT"], endpoint=self.put_aas_asset_information), + Rule("/asset-information/thumbnail", methods=["GET", "PUT", "DELETE"], + endpoint=self.not_implemented), + Rule("/submodel-refs", methods=["GET"], endpoint=self.get_aas_submodel_refs), + Rule("/submodel-refs", methods=["POST"], endpoint=self.post_aas_submodel_refs), + Rule("/submodel-refs/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_specific), + Submount("/submodels", [ + Rule("/", methods=["PUT"], + endpoint=self.put_aas_submodel_refs_submodel), + Rule("/", methods=["DELETE"], + endpoint=self.delete_aas_submodel_refs_submodel), + Rule("/", endpoint=self.aas_submodel_refs_redirect), + Rule("//", endpoint=self.aas_submodel_refs_redirect) ]) ]) ]), + Rule("/submodels", methods=["GET"], endpoint=self.get_submodel_all), + Rule("/submodels", methods=["POST"], endpoint=self.post_submodel), Submount("/submodels", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_all), - Rule("/", methods=["POST"], endpoint=self.post_submodel), - Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodel_all_metadata), - Rule("/$reference/", methods=["GET"], endpoint=self.get_submodel_all_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_all_metadata), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_all_reference), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["GET"], endpoint=self.get_submodel), + Rule("/", methods=["PUT"], endpoint=self.put_submodel), + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Submount("/", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel), - Rule("/", methods=["PUT"], endpoint=self.put_submodel), - Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$metadata/", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference/", methods=["GET"], endpoint=self.get_submodels_reference), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), + Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/submodel-elements", methods=["GET"], endpoint=self.get_submodel_submodel_elements), + Rule("/submodel-elements", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), Submount("/submodel-elements", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_elements), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/$metadata/", methods=["GET"], + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_metadata), - Rule("/$reference/", methods=["GET"], + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_submodel_elements_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/", methods=["GET"], + endpoint=self.get_submodel_submodel_elements_id_short_path), + Rule("/", methods=["POST"], + endpoint=self.post_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PUT"], + endpoint=self.put_submodel_submodel_elements_id_short_path), + Rule("/", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_elements_id_short_path), + Rule("/", methods=["PATCH"], endpoint=self.not_implemented), Submount("/", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_elements_id_short_path), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_elements_id_short_path), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_elements_id_short_path), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$metadata/", methods=["GET"], + Rule("/$metadata", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_metadata), - Rule("/$metadata/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$reference/", methods=["GET"], + Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$reference", methods=["GET"], endpoint=self.get_submodel_submodel_elements_id_short_path_reference), - Rule("/$value/", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value/", methods=["PATCH"], endpoint=self.not_implemented), - Rule("/$path/", methods=["GET"], endpoint=self.not_implemented), - Submount("/attachment", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_attachment), - Rule("/", methods=["PUT"], - endpoint=self.put_submodel_submodel_element_attachment), - Rule("/", methods=["DELETE"], - endpoint=self.delete_submodel_submodel_element_attachment) - ]), - Submount("/invoke", [ - Rule("/", methods=["POST"], endpoint=self.not_implemented), - Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) - ]), - Submount("/invoke-async", [ - Rule("/", methods=["POST"], endpoint=self.not_implemented), - Rule("/$value/", methods=["POST"], endpoint=self.not_implemented) - ]), - Submount("/operation-status", [ - Rule("//", methods=["GET"], - endpoint=self.not_implemented) - ]), + Rule("/$value", methods=["GET"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$path", methods=["GET"], endpoint=self.not_implemented), + Rule("/attachment", methods=["GET"], + endpoint=self.get_submodel_submodel_element_attachment), + Rule("/attachment", methods=["PUT"], + endpoint=self.put_submodel_submodel_element_attachment), + Rule("/attachment", methods=["DELETE"], + endpoint=self.delete_submodel_submodel_element_attachment), + Rule("/invoke", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke/$value", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke-async", methods=["POST"], endpoint=self.not_implemented), + Rule("/invoke-async/$value", methods=["POST"], endpoint=self.not_implemented), + Rule("/operation-status/", methods=["GET"], + endpoint=self.not_implemented), Submount("/operation-results", [ - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.not_implemented), - Rule("//$value/", methods=["GET"], + Rule("//$value", methods=["GET"], endpoint=self.not_implemented) ]), + Rule("/qualifiers", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("/qualifiers", methods=["POST"], + endpoint=self.post_submodel_submodel_element_qualifiers), Submount("/qualifiers", [ - Rule("/", methods=["GET"], - endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) ]), + Rule("/qualifiers", methods=["GET"], + endpoint=self.get_submodel_submodel_element_qualifiers), + Rule("/qualifiers", methods=["POST"], + endpoint=self.post_submodel_submodel_element_qualifiers), Submount("/qualifiers", [ - Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("/", methods=["POST"], - endpoint=self.post_submodel_submodel_element_qualifiers), - Rule("//", methods=["GET"], + Rule("/", methods=["GET"], endpoint=self.get_submodel_submodel_element_qualifiers), - Rule("//", methods=["PUT"], + Rule("/", methods=["PUT"], endpoint=self.put_submodel_submodel_element_qualifiers), - Rule("//", methods=["DELETE"], + Rule("/", methods=["DELETE"], endpoint=self.delete_submodel_submodel_element_qualifiers) ]) ]) @@ -542,7 +526,7 @@ def __init__(self, object_store: model.AbstractObjectStore, file_store: aasx.Abs ], converters={ "base64url": Base64URLConverter, "id_short_path": IdShortPathConverter - }) + }, strict_slashes=False) # TODO: the parameters can be typed via builtin wsgiref with Python 3.11+ def __call__(self, environ, start_response) -> Iterable[bytes]: