Skip to content

Commit 68ae2e6

Browse files
authored
AIOHTTPTransport default ssl cert validation add warning (#530)
1 parent b066e89 commit 68ae2e6

11 files changed

+628
-32
lines changed

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ include tox.ini
1212

1313
include gql/py.typed
1414

15-
recursive-include tests *.py *.graphql *.cnf *.yaml *.pem
15+
recursive-include tests *.py *.graphql *.cnf *.yaml *.pem *.crt
1616
recursive-include docs *.txt *.rst conf.py Makefile make.bat
1717
recursive-include docs/code_examples *.py
1818

gql/transport/aiohttp.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@
33
import io
44
import json
55
import logging
6+
import warnings
67
from ssl import SSLContext
7-
from typing import Any, AsyncGenerator, Callable, Dict, Optional, Tuple, Type, Union
8+
from typing import (
9+
Any,
10+
AsyncGenerator,
11+
Callable,
12+
Dict,
13+
Optional,
14+
Tuple,
15+
Type,
16+
Union,
17+
cast,
18+
)
819

920
import aiohttp
1021
from aiohttp.client_exceptions import ClientResponseError
@@ -46,7 +57,7 @@ def __init__(
4657
headers: Optional[LooseHeaders] = None,
4758
cookies: Optional[LooseCookies] = None,
4859
auth: Optional[Union[BasicAuth, "AppSyncAuthentication"]] = None,
49-
ssl: Union[SSLContext, bool, Fingerprint] = False,
60+
ssl: Union[SSLContext, bool, Fingerprint, str] = "ssl_warning",
5061
timeout: Optional[int] = None,
5162
ssl_close_timeout: Optional[Union[int, float]] = 10,
5263
json_serialize: Callable = json.dumps,
@@ -77,7 +88,20 @@ def __init__(
7788
self.headers: Optional[LooseHeaders] = headers
7889
self.cookies: Optional[LooseCookies] = cookies
7990
self.auth: Optional[Union[BasicAuth, "AppSyncAuthentication"]] = auth
80-
self.ssl: Union[SSLContext, bool, Fingerprint] = ssl
91+
92+
if ssl == "ssl_warning":
93+
ssl = False
94+
if str(url).startswith("https"):
95+
warnings.warn(
96+
"WARNING: By default, AIOHTTPTransport does not verify"
97+
" ssl certificates. This will be fixed in the next major version."
98+
" You can set ssl=True to force the ssl certificate verification"
99+
" or ssl=False to disable this warning"
100+
)
101+
102+
self.ssl: Union[SSLContext, bool, Fingerprint] = cast(
103+
Union[SSLContext, bool, Fingerprint], ssl
104+
)
81105
self.timeout: Optional[int] = timeout
82106
self.ssl_close_timeout: Optional[Union[int, float]] = ssl_close_timeout
83107
self.client_session_args = client_session_args

tests/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,29 @@ def get_localhost_ssl_context():
156156
return (testcert, ssl_context)
157157

158158

159+
def get_localhost_ssl_context_client():
160+
"""
161+
Create a client-side SSL context that verifies the specific self-signed certificate
162+
used for our test.
163+
"""
164+
# Get the certificate from the server setup
165+
cert_path = bytes(pathlib.Path(__file__).with_name("test_localhost_client.crt"))
166+
167+
# Create client SSL context
168+
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
169+
170+
# Load just the certificate part as a trusted CA
171+
ssl_context.load_verify_locations(cafile=cert_path)
172+
173+
# Require certificate verification
174+
ssl_context.verify_mode = ssl.CERT_REQUIRED
175+
176+
# Enable hostname checking for localhost
177+
ssl_context.check_hostname = True
178+
179+
return cert_path, ssl_context
180+
181+
159182
class WebSocketServer:
160183
"""Websocket server on localhost on a free port.
161184

tests/test_aiohttp.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
TransportServerError,
1515
)
1616

17-
from .conftest import TemporaryFile, strip_braces_spaces
17+
from .conftest import (
18+
TemporaryFile,
19+
get_localhost_ssl_context_client,
20+
strip_braces_spaces,
21+
)
1822

1923
query1_str = """
2024
query getContinents {
@@ -1285,7 +1289,10 @@ async def handler(request):
12851289

12861290
@pytest.mark.asyncio
12871291
@pytest.mark.parametrize("ssl_close_timeout", [0, 10])
1288-
async def test_aiohttp_query_https(event_loop, ssl_aiohttp_server, ssl_close_timeout):
1292+
@pytest.mark.parametrize("verify_https", ["disabled", "cert_provided"])
1293+
async def test_aiohttp_query_https(
1294+
event_loop, ssl_aiohttp_server, ssl_close_timeout, verify_https
1295+
):
12891296
from aiohttp import web
12901297
from gql.transport.aiohttp import AIOHTTPTransport
12911298

@@ -1300,8 +1307,20 @@ async def handler(request):
13001307

13011308
assert str(url).startswith("https://")
13021309

1310+
extra_args = {}
1311+
1312+
if verify_https == "cert_provided":
1313+
_, ssl_context = get_localhost_ssl_context_client()
1314+
1315+
extra_args["ssl"] = ssl_context
1316+
elif verify_https == "disabled":
1317+
extra_args["ssl"] = False
1318+
13031319
transport = AIOHTTPTransport(
1304-
url=url, timeout=10, ssl_close_timeout=ssl_close_timeout
1320+
url=url,
1321+
timeout=10,
1322+
ssl_close_timeout=ssl_close_timeout,
1323+
**extra_args,
13051324
)
13061325

13071326
async with Client(transport=transport) as session:
@@ -1318,6 +1337,65 @@ async def handler(request):
13181337
assert africa["code"] == "AF"
13191338

13201339

1340+
@pytest.mark.skip(reason="We will change the default to fix this in a future version")
1341+
@pytest.mark.asyncio
1342+
async def test_aiohttp_query_https_self_cert_fail(event_loop, ssl_aiohttp_server):
1343+
"""By default, we should verify the ssl certificate"""
1344+
from aiohttp.client_exceptions import ClientConnectorCertificateError
1345+
from aiohttp import web
1346+
from gql.transport.aiohttp import AIOHTTPTransport
1347+
1348+
async def handler(request):
1349+
return web.Response(text=query1_server_answer, content_type="application/json")
1350+
1351+
app = web.Application()
1352+
app.router.add_route("POST", "/", handler)
1353+
server = await ssl_aiohttp_server(app)
1354+
1355+
url = server.make_url("/")
1356+
1357+
assert str(url).startswith("https://")
1358+
1359+
transport = AIOHTTPTransport(url=url, timeout=10)
1360+
1361+
with pytest.raises(ClientConnectorCertificateError) as exc_info:
1362+
async with Client(transport=transport) as session:
1363+
query = gql(query1_str)
1364+
1365+
# Execute query asynchronously
1366+
await session.execute(query)
1367+
1368+
expected_error = "certificate verify failed: self-signed certificate"
1369+
1370+
assert expected_error in str(exc_info.value)
1371+
assert transport.session is None
1372+
1373+
1374+
@pytest.mark.asyncio
1375+
async def test_aiohttp_query_https_self_cert_warn(event_loop, ssl_aiohttp_server):
1376+
from aiohttp import web
1377+
from gql.transport.aiohttp import AIOHTTPTransport
1378+
1379+
async def handler(request):
1380+
return web.Response(text=query1_server_answer, content_type="application/json")
1381+
1382+
app = web.Application()
1383+
app.router.add_route("POST", "/", handler)
1384+
server = await ssl_aiohttp_server(app)
1385+
1386+
url = server.make_url("/")
1387+
1388+
assert str(url).startswith("https://")
1389+
1390+
expected_warning = (
1391+
"WARNING: By default, AIOHTTPTransport does not verify ssl certificates."
1392+
" This will be fixed in the next major version."
1393+
)
1394+
1395+
with pytest.warns(Warning, match=expected_warning):
1396+
AIOHTTPTransport(url=url, timeout=10)
1397+
1398+
13211399
@pytest.mark.asyncio
13221400
async def test_aiohttp_error_fetching_schema(event_loop, aiohttp_server):
13231401
from aiohttp import web

tests/test_aiohttp_websocket_query.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import json
3-
import ssl
43
import sys
54
from typing import Dict, Mapping
65

@@ -14,7 +13,7 @@
1413
TransportServerError,
1514
)
1615

17-
from .conftest import MS, WebSocketServerHelper
16+
from .conftest import MS, WebSocketServerHelper, get_localhost_ssl_context_client
1817

1918
# Marking all tests in this file with the aiohttp AND websockets marker
2019
pytestmark = pytest.mark.aiohttp
@@ -92,8 +91,9 @@ async def test_aiohttp_websocket_starting_client_in_context_manager(
9291
@pytest.mark.websockets
9392
@pytest.mark.parametrize("ws_ssl_server", [server1_answers], indirect=True)
9493
@pytest.mark.parametrize("ssl_close_timeout", [0, 10])
94+
@pytest.mark.parametrize("verify_https", ["disabled", "cert_provided"])
9595
async def test_aiohttp_websocket_using_ssl_connection(
96-
event_loop, ws_ssl_server, ssl_close_timeout
96+
event_loop, ws_ssl_server, ssl_close_timeout, verify_https
9797
):
9898

9999
from gql.transport.aiohttp_websockets import AIOHTTPWebsocketsTransport
@@ -103,11 +103,19 @@ async def test_aiohttp_websocket_using_ssl_connection(
103103
url = f"wss://{server.hostname}:{server.port}/graphql"
104104
print(f"url = {url}")
105105

106-
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
107-
ssl_context.load_verify_locations(ws_ssl_server.testcert)
106+
extra_args = {}
107+
108+
if verify_https == "cert_provided":
109+
_, ssl_context = get_localhost_ssl_context_client()
110+
111+
extra_args["ssl"] = ssl_context
112+
elif verify_https == "disabled":
113+
extra_args["ssl"] = False
108114

109115
transport = AIOHTTPWebsocketsTransport(
110-
url=url, ssl=ssl_context, ssl_close_timeout=ssl_close_timeout
116+
url=url,
117+
ssl_close_timeout=ssl_close_timeout,
118+
**extra_args,
111119
)
112120

113121
async with Client(transport=transport) as session:
@@ -130,6 +138,49 @@ async def test_aiohttp_websocket_using_ssl_connection(
130138
assert transport.websocket is None
131139

132140

141+
@pytest.mark.asyncio
142+
@pytest.mark.websockets
143+
@pytest.mark.parametrize("ws_ssl_server", [server1_answers], indirect=True)
144+
@pytest.mark.parametrize("ssl_close_timeout", [10])
145+
@pytest.mark.parametrize("verify_https", ["explicitely_enabled", "default"])
146+
async def test_aiohttp_websocket_using_ssl_connection_self_cert_fail(
147+
event_loop, ws_ssl_server, ssl_close_timeout, verify_https
148+
):
149+
150+
from aiohttp.client_exceptions import ClientConnectorCertificateError
151+
from gql.transport.aiohttp_websockets import AIOHTTPWebsocketsTransport
152+
153+
server = ws_ssl_server
154+
155+
url = f"wss://{server.hostname}:{server.port}/graphql"
156+
print(f"url = {url}")
157+
158+
extra_args = {}
159+
160+
if verify_https == "explicitely_enabled":
161+
extra_args["ssl"] = True
162+
163+
transport = AIOHTTPWebsocketsTransport(
164+
url=url,
165+
ssl_close_timeout=ssl_close_timeout,
166+
**extra_args,
167+
)
168+
169+
with pytest.raises(ClientConnectorCertificateError) as exc_info:
170+
async with Client(transport=transport) as session:
171+
172+
query1 = gql(query1_str)
173+
174+
await session.execute(query1)
175+
176+
expected_error = "certificate verify failed: self-signed certificate"
177+
178+
assert expected_error in str(exc_info.value)
179+
180+
# Check client is disconnect here
181+
assert transport.websocket is None
182+
183+
133184
@pytest.mark.asyncio
134185
@pytest.mark.websockets
135186
@pytest.mark.parametrize("server", [server1_answers], indirect=True)

0 commit comments

Comments
 (0)