From c767cd4f1b217cef41a03371b14b626c37269c27 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 12:33:54 -0400 Subject: [PATCH 1/7] Fix SRV hostname validation fails --- pymongo/asynchronous/srv_resolver.py | 2 +- pymongo/synchronous/srv_resolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index f7c67af3e1..9d53e0977f 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -137,7 +137,7 @@ async def _get_srv_response_and_hosts( # Validate hosts for node in nodes: - if self.__fqdn == node[0].lower(): + if self.__fqdn == node[0].lower() and len(node[0].split(".")) < 3: raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index cf7b0842ab..2f3d11af82 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -137,7 +137,7 @@ def _get_srv_response_and_hosts( # Validate hosts for node in nodes: - if self.__fqdn == node[0].lower(): + if self.__fqdn == node[0].lower() and len(node[0].split(".")) < 3: raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) From c2bc931d5ddd5aa1ad51c22b14dfdf458f1cc0e2 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 13:16:22 -0400 Subject: [PATCH 2/7] Review fixes --- pymongo/asynchronous/srv_resolver.py | 3 ++- pymongo/synchronous/srv_resolver.py | 3 ++- test/asynchronous/test_dns.py | 17 +++++++++++++---- test/test_dns.py | 17 +++++++++++++---- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 9d53e0977f..ee06212622 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -137,7 +137,8 @@ async def _get_srv_response_and_hosts( # Validate hosts for node in nodes: - if self.__fqdn == node[0].lower() and len(node[0].split(".")) < 3: + srv_host = node[0].lower() + if self.__fqdn == srv_host and len(srv_host.split(".")) < 3: raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 2f3d11af82..bd8330b4d1 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -137,7 +137,8 @@ def _get_srv_response_and_hosts( # Validate hosts for node in nodes: - if self.__fqdn == node[0].lower() and len(node[0].split(".")) < 3: + srv_host = node[0].lower() + if self.__fqdn == srv_host and len(srv_host.split(".")) < 3: raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index 01c8d7b40b..60af0369a6 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -220,12 +220,15 @@ async def mock_resolve(query, record_type, *args, **kwargs): mock_resolver.side_effect = mock_resolve domain = case["query"].split("._tcp.")[1] connection_string = f"mongodb+srv://{domain}" - try: + if "expected_error" not in case: await parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") + try: + await parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") async def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): with patch("dns.asyncresolver.resolve"): @@ -264,6 +267,12 @@ async def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "mock_target": "mongo.local", "expected_error": "Invalid SRV host", }, + # When the SRV hostname has three or more dot-separated parts + # it is valid for the returned hostnames to be identical. + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.mongodb.com", + }, ] await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) diff --git a/test/test_dns.py b/test/test_dns.py index 9360f3f289..822ef3f748 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -218,12 +218,15 @@ def mock_resolve(query, record_type, *args, **kwargs): mock_resolver.side_effect = mock_resolve domain = case["query"].split("._tcp.")[1] connection_string = f"mongodb+srv://{domain}" - try: + if "expected_error" not in case: parse_uri(connection_string) - except ConfigurationError as e: - self.assertIn(case["expected_error"], str(e)) else: - self.fail(f"ConfigurationError was not raised for query: {case['query']}") + try: + parse_uri(connection_string) + except ConfigurationError as e: + self.assertIn(case["expected_error"], str(e)) + else: + self.fail(f"ConfigurationError was not raised for query: {case['query']}") def test_1_allow_srv_hosts_with_fewer_than_three_dot_separated_parts(self): with patch("dns.resolver.resolve"): @@ -262,6 +265,12 @@ def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "mock_target": "mongo.local", "expected_error": "Invalid SRV host", }, + # When the SRV hostname has three or more dot-separated parts + # it is valid for the returned hostnames to be identical. + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.mongodb.com", + }, ] self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) From 66d045eebf858307dac1c2a5b05ef72f1cc41036 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 15:00:18 -0400 Subject: [PATCH 3/7] Check length of fqdn not srv host --- pymongo/asynchronous/srv_resolver.py | 3 ++- pymongo/synchronous/srv_resolver.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index ee06212622..8e80c92b73 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -96,6 +96,7 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) + self.nparts = len(self.__fqdn.split(".")) async def get_options(self) -> Optional[str]: from dns import resolver @@ -138,7 +139,7 @@ async def _get_srv_response_and_hosts( # Validate hosts for node in nodes: srv_host = node[0].lower() - if self.__fqdn == srv_host and len(srv_host.split(".")) < 3: + if self.__fqdn == srv_host and self.nparts < 3: raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index bd8330b4d1..7ac2dd178e 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -96,6 +96,7 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) + self.nparts = len(self.__fqdn.split(".")) def get_options(self) -> Optional[str]: from dns import resolver @@ -138,7 +139,7 @@ def _get_srv_response_and_hosts( # Validate hosts for node in nodes: srv_host = node[0].lower() - if self.__fqdn == srv_host and len(srv_host.split(".")) < 3: + if self.__fqdn == srv_host and self.nparts < 3: raise ConfigurationError( "Invalid SRV host: return address is identical to SRV hostname" ) From 0f1f8a7af84c9f5f76f17d32929a73be79346913 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 15:05:58 -0400 Subject: [PATCH 4/7] Move test case to separate test --- test/asynchronous/test_dns.py | 17 +++++++++++------ test/test_dns.py | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/test/asynchronous/test_dns.py b/test/asynchronous/test_dns.py index 60af0369a6..5666612218 100644 --- a/test/asynchronous/test_dns.py +++ b/test/asynchronous/test_dns.py @@ -267,12 +267,6 @@ async def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "mock_target": "mongo.local", "expected_error": "Invalid SRV host", }, - # When the SRV hostname has three or more dot-separated parts - # it is valid for the returned hostnames to be identical. - { - "query": "_mongodb._tcp.blogs.mongodb.com", - "mock_target": "blogs.mongodb.com", - }, ] await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) @@ -298,6 +292,17 @@ async def test_4_throw_when_return_address_does_not_contain_dot_separating_share ] await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) + async def test_5_when_srv_hostname_has_two_dot_separated_parts_it_is_valid_for_the_returned_hostname_to_be_identical( + self + ): + test_cases = [ + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.mongodb.com", + }, + ] + await self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) + if __name__ == "__main__": unittest.main() diff --git a/test/test_dns.py b/test/test_dns.py index 822ef3f748..8f88562e3f 100644 --- a/test/test_dns.py +++ b/test/test_dns.py @@ -265,12 +265,6 @@ def test_3_throw_when_return_address_is_identical_to_srv_hostname(self): "mock_target": "mongo.local", "expected_error": "Invalid SRV host", }, - # When the SRV hostname has three or more dot-separated parts - # it is valid for the returned hostnames to be identical. - { - "query": "_mongodb._tcp.blogs.mongodb.com", - "mock_target": "blogs.mongodb.com", - }, ] self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) @@ -296,6 +290,17 @@ def test_4_throw_when_return_address_does_not_contain_dot_separating_shared_part ] self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) + def test_5_when_srv_hostname_has_two_dot_separated_parts_it_is_valid_for_the_returned_hostname_to_be_identical( + self + ): + test_cases = [ + { + "query": "_mongodb._tcp.blogs.mongodb.com", + "mock_target": "blogs.mongodb.com", + }, + ] + self.run_initial_dns_seedlist_discovery_prose_tests(test_cases) + if __name__ == "__main__": unittest.main() From 03ccebee6817663fd6f65f451fb611bc86119f84 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 15:13:31 -0400 Subject: [PATCH 5/7] Use newly defined srv_host --- pymongo/asynchronous/srv_resolver.py | 2 +- pymongo/synchronous/srv_resolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 8e80c92b73..6821b6c904 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -144,7 +144,7 @@ async def _get_srv_response_and_hosts( "Invalid SRV host: return address is identical to SRV hostname" ) try: - nlist = node[0].lower().split(".")[1:][-self.__slen :] + nlist = srv_host.split(".")[1:][-self.__slen :] except Exception: raise ConfigurationError(f"Invalid SRV host: {node[0]}") from None if self.__plist != nlist: diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 7ac2dd178e..7eedeb1fec 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -144,7 +144,7 @@ def _get_srv_response_and_hosts( "Invalid SRV host: return address is identical to SRV hostname" ) try: - nlist = node[0].lower().split(".")[1:][-self.__slen :] + nlist = srv_host.split(".")[1:][-self.__slen :] except Exception: raise ConfigurationError(f"Invalid SRV host: {node[0]}") from None if self.__plist != nlist: From 0dc962b661adabc980abbaba420a406572d6a5e3 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 15:40:10 -0400 Subject: [PATCH 6/7] Use split_fqdn --- pymongo/asynchronous/srv_resolver.py | 2 +- pymongo/synchronous/srv_resolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/srv_resolver.py b/pymongo/asynchronous/srv_resolver.py index 6821b6c904..9d1b8fe141 100644 --- a/pymongo/asynchronous/srv_resolver.py +++ b/pymongo/asynchronous/srv_resolver.py @@ -96,7 +96,7 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) - self.nparts = len(self.__fqdn.split(".")) + self.nparts = len(split_fqdn) async def get_options(self) -> Optional[str]: from dns import resolver diff --git a/pymongo/synchronous/srv_resolver.py b/pymongo/synchronous/srv_resolver.py index 7eedeb1fec..0817c6dcd7 100644 --- a/pymongo/synchronous/srv_resolver.py +++ b/pymongo/synchronous/srv_resolver.py @@ -96,7 +96,7 @@ def __init__( except Exception: raise ConfigurationError(_INVALID_HOST_MSG % (fqdn,)) from None self.__slen = len(self.__plist) - self.nparts = len(self.__fqdn.split(".")) + self.nparts = len(split_fqdn) def get_options(self) -> Optional[str]: from dns import resolver From e474e2232efa8e58d694d137ca842bcb7a71c9e3 Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark" Date: Wed, 9 Apr 2025 16:02:59 -0400 Subject: [PATCH 7/7] Add changelog entry --- doc/changelog.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 077c85bb4b..3c307564b1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,22 @@ Changelog ========= +Changes in Version 4.12.1 (XXXX/XX/XX) +-------------------------------------- + +Version 4.12.1 is a bug fix release. + +- Fixed a bug causing SRV hostname validation to fail when resolver and resolved hostnames are identical with three domain levels. + +Issues Resolved +............... + +See the `PyMongo 4.12 release notes in JIRA`_ for the list of resolved issues +in this release. + +.. _PyMongo 4.12 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=41916 +.. _PYTHON-5288: https://jira.mongodb.org/browse/PYTHON-5288 + Changes in Version 4.12.0 (2025/04/08) --------------------------------------