From d5eaab3bb36114759861c98a419ecca6955d41ce Mon Sep 17 00:00:00 2001 From: Raitis Veinbahs Date: Wed, 4 Sep 2024 11:54:34 +0300 Subject: [PATCH] feat: respect X-Forwarded-Proto (DEV-3499) (#453) --- shttps/Connection.cpp | 11 +++- shttps/Connection.h | 2 +- src/SipiHttpServer.cpp | 27 +++----- test/e2e/conftest.py | 6 +- test/e2e/test_02_server.py | 124 +++++++++++++++++++------------------ 5 files changed, 85 insertions(+), 85 deletions(-) diff --git a/shttps/Connection.cpp b/shttps/Connection.cpp index 6ff15902..d214527d 100644 --- a/shttps/Connection.cpp +++ b/shttps/Connection.cpp @@ -867,6 +867,15 @@ Connection::~Connection() } //============================================================================= +/*! + * Return true if a secure (SSL) connection is used + */ +bool Connection::secure(void) +{ + bool forwarded = !header("x-forwarded-proto").compare("https"); + return _secure || forwarded; +} + int Connection::setupKeepAlive(int default_timeout) { if (_keep_alive) { @@ -1607,4 +1616,4 @@ bool Connection::cleanupUploads(void) } //============================================================================= -} +}// namespace shttps diff --git a/shttps/Connection.h b/shttps/Connection.h index aae32620..89662440 100644 --- a/shttps/Connection.h +++ b/shttps/Connection.h @@ -428,7 +428,7 @@ class Connection /*! * Return true if a secure (SSL) connection is used */ - inline bool secure(void) { return _secure; } + bool secure(void); /*! * Set the secure connection status diff --git a/src/SipiHttpServer.cpp b/src/SipiHttpServer.cpp index 5e25fa76..cff7b192 100644 --- a/src/SipiHttpServer.cpp +++ b/src/SipiHttpServer.cpp @@ -657,20 +657,13 @@ static void serve_info_json_file(Connection &conn_obj, json_object_set_new(root, "@context", json_string("http://sipi.io/api/file/3/context.json")); } + std::string proto = conn_obj.secure() ? std::string("https://") : std::string("http://"); std::string host = conn_obj.header("host"); std::string id; if (params[iiif_prefix] == "") { - if (conn_obj.secure()) { - id = std::string("https://") + host + "/" + params[iiif_identifier]; - } else { - id = std::string("http://") + host + "/" + params[iiif_identifier]; - } + id = proto + host + "/" + params[iiif_identifier]; } else { - if (conn_obj.secure()) { - id = std::string("https://") + host + "/" + params[iiif_prefix] + "/" + params[iiif_identifier]; - } else { - id = std::string("http://") + host + "/" + params[iiif_prefix] + "/" + params[iiif_identifier]; - } + id = proto + host + "/" + params[iiif_prefix] + "/" + params[iiif_identifier]; } json_object_set_new(root, "id", json_string(id.c_str())); @@ -927,20 +920,14 @@ static void serve_knora_json_file(Connection &conn_obj, json_t *root = json_object(); json_object_set_new(root, "@context", json_string("http://sipi.io/api/file/3/context.json")); + std::string proto = conn_obj.secure() ? std::string("https://") : std::string("http://"); std::string host = conn_obj.header("host"); std::string id; + if (params[iiif_prefix] == "") { - if (conn_obj.secure()) { - id = std::string("https://") + host + "/" + params[iiif_identifier]; - } else { - id = std::string("http://") + host + "/" + params[iiif_identifier]; - } + id = proto + host + "/" + params[iiif_identifier]; } else { - if (conn_obj.secure()) { - id = std::string("https://") + host + "/" + params[iiif_prefix] + "/" + params[iiif_identifier]; - } else { - id = std::string("http://") + host + "/" + params[iiif_prefix] + "/" + params[iiif_identifier]; - } + id = proto + host + "/" + params[iiif_prefix] + "/" + params[iiif_identifier]; } json_object_set_new(root, "id", json_string(id.c_str())); diff --git a/test/e2e/conftest.py b/test/e2e/conftest.py index e2d6fa69..bf99b361 100644 --- a/test/e2e/conftest.py +++ b/test/e2e/conftest.py @@ -411,7 +411,7 @@ def post_file(self, url_path, file_path, mime_type, params=None, headers=None): return response.json() - def get_json(self, url_path, use_ssl=False): + def get_json(self, url_path, use_ssl=False, use_forwarded_ssl=None): """ Sends a request which expects JSON :param url_path: a path that will be appended to the Sipi base URL to make the request. @@ -423,8 +423,10 @@ def get_json(self, url_path, use_ssl=False): else: sipi_url = self.make_sipi_url(url_path) + x_forwarded_proto = {True: 'https', False: 'http'}.get(use_forwarded_ssl) + try: - response = requests.get(sipi_url) + response = requests.get(sipi_url, headers={'X-Forwarded-Proto': x_forwarded_proto}) response.raise_for_status() except: raise SipiTestError("post request to {} failed: {}".format(sipi_url, response.json()["message"])) diff --git a/test/e2e/test_02_server.py b/test/e2e/test_02_server.py index c6abef03..0d353aed 100644 --- a/test/e2e/test_02_server.py +++ b/test/e2e/test_02_server.py @@ -336,78 +336,80 @@ def test_knora_info_validation(self, manager): def test_json_info_validation(self, manager): """pass the info.json request tests""" - expected_result = { - '@context': 'http://iiif.io/api/image/3/context.json', - 'id': 'http://127.0.0.1:1024/unit/_lena512.jp2', - 'type': 'ImageService3', - 'protocol': 'http://iiif.io/api/image', - 'profile': 'level2', - 'width': 512, - 'height': 512, - 'sizes': [ - {'width': 256, 'height': 256}, - {'width': 128, 'height': 128} - ], - 'tiles': [{'width': 512, 'height': 512, 'scaleFactors': [1, 2, 3, 4]}], - 'extraFormats': ['tif', 'jp2'], - 'preferredFormats': ['jpg', 'tif', 'jp2', 'png'], - 'extraFeatures': [ - 'baseUriRedirect', - 'canonicalLinkHeader', - 'cors', - 'jsonldMediaType', - 'mirroring', - 'profileLinkHeader', - 'regionByPct', - 'regionByPx', - 'regionSquare', - 'rotationArbitrary', - 'rotationBy90s', - 'sizeByConfinedWh', - 'sizeByH', - 'sizeByPct', - 'sizeByW', - 'sizeByWh', - 'sizeUpscaling' - ] - } + def expected_result(filename, proto='http'): + return { + '@context': 'http://iiif.io/api/image/3/context.json', + 'id': proto + '://127.0.0.1:1024/unit/' + filename, + 'type': 'ImageService3', + 'protocol': 'http://iiif.io/api/image', + 'profile': 'level2', + 'width': 512, + 'height': 512, + 'sizes': [ + {'width': 256, 'height': 256}, + {'width': 128, 'height': 128} + ], + 'tiles': [{'width': 512, 'height': 512, 'scaleFactors': [1, 2, 3, 4]}], + 'extraFormats': ['tif', 'jp2'], + 'preferredFormats': ['jpg', 'tif', 'jp2', 'png'], + 'extraFeatures': [ + 'baseUriRedirect', + 'canonicalLinkHeader', + 'cors', + 'jsonldMediaType', + 'mirroring', + 'profileLinkHeader', + 'regionByPct', + 'regionByPx', + 'regionSquare', + 'rotationArbitrary', + 'rotationBy90s', + 'sizeByConfinedWh', + 'sizeByH', + 'sizeByPct', + 'sizeByW', + 'sizeByWh', + 'sizeUpscaling' + ] + } - response_json = manager.post_file( - "/api/upload", manager.data_dir_path("unit/lena512.tif"), "image/tiff") - filename = response_json["filename"] + response_json = manager.post_file("/api/upload", manager.data_dir_path("unit/lena512.tif"), "image/tiff") - manager.expect_status_code( - "/unit/{}/full/max/0/default.jpg".format(filename), 200) + filename = response_json["filename"] + manager.expect_status_code("/unit/{}/full/max/0/default.jpg".format(filename), 200) response_json = manager.get_json("/unit/{}/info.json".format(filename)) - expected_result["id"] = "http://127.0.0.1:1024/unit/{}".format( - filename) - assert response_json == expected_result + assert response_json == expected_result(filename) # response_json = manager.get_json("/unit/{}/info.json".format(filename), use_ssl=True) - # expected_result["id"] = "https://127.0.0.1:1024/unit/{}".format(filename) - # assert response_json == expected_result + # assert response_json == expected_result(filename) + + response_json = manager.get_json("/unit/{}/info.json".format(filename), use_forwarded_ssl=False) + assert response_json == expected_result(filename, proto='http') + + response_json = manager.get_json("/unit/{}/info.json".format(filename), use_forwarded_ssl=True) + assert response_json == expected_result(filename, proto='https') def test_knora_json_for_video(self, manager): """pass the knora.json request for video""" - expected_result = { - "@context": "http://sipi.io/api/file/3/context.json", - "id": "http://127.0.0.1:1024/unit/8pdET49BfoJ-EeRcIbgcLch.mp4", - "checksumOriginal": "19cc4bccad39c89cc44936ef69565bb933d41a065fd59d666d58e5ef344e8149", - "checksumDerivative": "19cc4bccad39c89cc44936ef69565bb933d41a065fd59d666d58e5ef344e8149", - "internalMimeType": "video/mp4", - "fileSize": 475205, - "originalFilename": "Dummy.mp4", - "duration": 4.7000000000000002, - "fps": 30, - "height": 240, - "width": 320 - } + def expected_result(proto): + return { + "@context": "http://sipi.io/api/file/3/context.json", + "id": proto + "://127.0.0.1:1024/unit/8pdET49BfoJ-EeRcIbgcLch.mp4", + "checksumOriginal": "19cc4bccad39c89cc44936ef69565bb933d41a065fd59d666d58e5ef344e8149", + "checksumDerivative": "19cc4bccad39c89cc44936ef69565bb933d41a065fd59d666d58e5ef344e8149", + "internalMimeType": "video/mp4", + "fileSize": 475205, + "originalFilename": "Dummy.mp4", + "duration": 4.7000000000000002, + "fps": 30, + "height": 240, + "width": 320 + } - response_json = manager.get_json( - "/unit/8pdET49BfoJ-EeRcIbgcLch.mp4/knora.json") - assert response_json == expected_result + assert manager.get_json("/unit/8pdET49BfoJ-EeRcIbgcLch.mp4/knora.json") == expected_result('http') + assert manager.get_json("/unit/8pdET49BfoJ-EeRcIbgcLch.mp4/knora.json", use_forwarded_ssl=True) == expected_result('https') def test_handling_of_missing_sidecar_file_for_video(self, manager): """correctly handle missing sidecar file for video"""