Repository: opensearch-project/opensearch-py (python)

Issue:
[BUG] Async client is missing ssl_assert_hostname implementation
### What is the bug?
The `ssl_assert_hostname` parameter has no effect on the `AsyncOpenSearch` client.

### How can one reproduce the bug?
```python
client = AsyncOpenSearch(hosts='my.opensearch.with.selfsigned.certs', ssl_assert_hostname=False)
await client.search(...)
```
```
opensearchpy.exceptions.ConnectionError: ConnectionError(Cannot connect to host my.opensearch.instance:9200 ssl:True [SSLCertVerificationError: (1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'my.opensearch.instance'. (_ssl.c:1000)")]) caused by: ClientConnectorCertificateError(Cannot connect to host my.opensearch.instance:9200 ssl:True [SSLCertVerificationError: (1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'my.opensearch.instance'. (_ssl.c:1000)")])
```

### What is the expected behavior?
The connection should succeed even if the hostname does not match.

### What is your host/environment?
N/A

### Do you have any screenshots?
N/A

### Do you have any additional context?
The parameter is documented in the function docs:
https://github.com/opensearch-project/opensearch-py/blob/6fa54a719a399cdcd8d970605487b9e14d257383/opensearchpy/_async/http_aiohttp.py#L120

I believe the parameter works with the `urllib3`-based client.

I'm working on a PR to fix this.


Pull request description:
Implement AsyncOpenSearch() parameter `ssl_assert_hostname`
### Description
Implements the `ssl_assert_hostname` parameter for `AsyncOpenSearch()`.

### Issues Resolved
Closes #842

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2430c8bf..8f11b945 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 ## [Unreleased]
 ### Added
 - Added `AsyncSearch#collapse` ([827](https://github.com/opensearch-project/opensearch-py/pull/827))
-- Support `pool_maxsize` in `AsyncOpenSearch` ([845](https://github.com/opensearch-project/opensearch-py/pull/845))
+- Added `pool_maxsize` to `AsyncOpenSearch` ([845](https://github.com/opensearch-project/opensearch-py/pull/845))
+- Added `ssl_assert_hostname` to `AsyncOpenSearch` ([843](https://github.com/opensearch-project/opensearch-py/pull/843))
 ### Changed
 ### Deprecated
 ### Removed
diff --git a/docs/source/api-ref/clients/opensearch_client.md b/docs/source/api-ref/clients/opensearch_client.md
index 15838f29..4c0845df 100644
--- a/docs/source/api-ref/clients/opensearch_client.md
+++ b/docs/source/api-ref/clients/opensearch_client.md
@@ -3,3 +3,7 @@
 ```{eval-rst}
 .. autoclass:: opensearchpy.OpenSearch
 ```
+
+```{eval-rst}
+.. autoclass:: opensearchpy.AsyncOpenSearch
+```
diff --git a/docs/source/api-ref/connection.md b/docs/source/api-ref/connection.md
index 8ac0e3ec..3a6ea4b0 100644
--- a/docs/source/api-ref/connection.md
+++ b/docs/source/api-ref/connection.md
@@ -1,4 +1,4 @@
-# connection 
+# Connection Types 
 
 ```{eval-rst}
 .. autoclass:: opensearchpy.Connection
@@ -12,6 +12,10 @@
 .. autoclass:: opensearchpy.Urllib3HttpConnection
 ```
 
+```{eval-rst}
+.. autoclass:: opensearchpy.AIOHttpConnection
+```
+
 ```{eval-rst}
 .. autoclass:: opensearchpy.connections
 ```
\ No newline at end of file
diff --git a/opensearchpy/_async/client/__init__.py b/opensearchpy/_async/client/__init__.py
index 9cd54d38..24b95498 100644
--- a/opensearchpy/_async/client/__init__.py
+++ b/opensearchpy/_async/client/__init__.py
@@ -109,7 +109,7 @@ class AsyncOpenSearch(Client):
         ])
 
     If using SSL, there are several parameters that control how we deal with
-    certificates (see :class:`~opensearchpy.Urllib3HttpConnection` for
+    certificates (see :class:`~opensearchpy.AIOHttpConnection` for
     detailed description of the options)::
 
         client = OpenSearch(
@@ -123,7 +123,7 @@ class AsyncOpenSearch(Client):
         )
 
     If using SSL, but don't verify the certs, a warning message is showed
-    optionally (see :class:`~opensearchpy.Urllib3HttpConnection` for
+    optionally (see :class:`~opensearchpy.AIOHttpConnection` for
     detailed description of the options)::
 
         client = OpenSearch(
@@ -132,12 +132,14 @@ class AsyncOpenSearch(Client):
             use_ssl=True,
             # no verify SSL certificates
             verify_certs=False,
+            # don't verify the hostname in the certificate
+            ssl_assert_hostname=False,
             # don't show warnings about ssl certs verification
             ssl_show_warn=False
         )
 
     SSL client authentication is supported
-    (see :class:`~opensearchpy.Urllib3HttpConnection` for
+    (see :class:`~opensearchpy.AIOHttpConnection` for
     detailed description of the options)::
 
         client = OpenSearch(
diff --git a/opensearchpy/_async/http_aiohttp.py b/opensearchpy/_async/http_aiohttp.py
index ed1d26e7..1de47634 100644
--- a/opensearchpy/_async/http_aiohttp.py
+++ b/opensearchpy/_async/http_aiohttp.py
@@ -85,6 +85,7 @@ class AIOHttpConnection(AsyncConnection):
         client_cert: Any = None,
         client_key: Any = None,
         ssl_version: Any = None,
+        ssl_assert_hostname: bool = True,
         ssl_assert_fingerprint: Any = None,
         maxsize: Optional[int] = 10,
         headers: Any = None,
@@ -178,7 +179,7 @@ class AIOHttpConnection(AsyncConnection):
 
             if verify_certs:
                 ssl_context.verify_mode = ssl.CERT_REQUIRED
-                ssl_context.check_hostname = True
+                ssl_context.check_hostname = ssl_assert_hostname
             else:
                 ssl_context.check_hostname = False
                 ssl_context.verify_mode = ssl.CERT_NONE


Test patch:
diff --git a/test_opensearchpy/test_async/test_connection.py b/test_opensearchpy/test_async/test_connection.py
index 5c8f6e26..c7d7b4a3 100644
--- a/test_opensearchpy/test_async/test_connection.py
+++ b/test_opensearchpy/test_async/test_connection.py
@@ -29,6 +29,7 @@ import gzip
 import io
 import json
 import ssl
+import sys
 import warnings
 from platform import python_version
 from typing import Any
@@ -97,6 +98,17 @@ class TestAIOHttpConnection:
         assert con.use_ssl
         assert con.session.connector._ssl == context
 
+    async def test_ssl_assert_hostname(self) -> None:
+        con = AIOHttpConnection(use_ssl=True, ssl_assert_hostname=True)
+        await con._create_aiohttp_session()
+        assert con.use_ssl
+        assert con.session.connector._ssl.check_hostname is True
+
+        con = AIOHttpConnection(use_ssl=True, ssl_assert_hostname=False)
+        await con._create_aiohttp_session()
+        assert con.use_ssl
+        assert con.session.connector._ssl.check_hostname is False
+
     async def test_opaque_id(self) -> None:
         con = AIOHttpConnection(opaque_id="app-1")
         assert con.headers["x-opaque-id"] == "app-1"
@@ -217,7 +229,15 @@ class TestAIOHttpConnection:
                 use_ssl=True, verify_certs=False, ssl_show_warn=False
             )
             await con._create_aiohttp_session()
-            assert w == []
+            if sys.hexversion < 0x30C0700:
+                assert w == []
+            else:
+                assert len(w) == 1
+                assert (
+                    str(w[0].message) == "enable_cleanup_closed ignored because "
+                    "https://github.com/python/cpython/pull/118960 is fixed in "
+                    "Python version sys.version_info(major=3, minor=12, micro=7, releaselevel='final', serial=0)"
+                )
 
         assert isinstance(con.session, aiohttp.ClientSession)