Skip to content

Commit cff4301

Browse files
authored
Merge pull request #54 from lawtancool/copilot/fix-blocking-call-load-default-certs
Pass caller's session to socketio to avoid blocking SSL calls in event loop
2 parents 3284996 + faf358a commit cff4301

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
__pycache__/
44
login_info.py
55
allitems.json
6+
*.egg-info/

pyControl4/websocket.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,16 @@ def __init__(
101101
Parameters:
102102
`ip` - The IP address of the Control4 Director/Controller.
103103
104-
`session` - (Optional) Allows the use of an
104+
`session_no_verify_ssl` - (Optional) Allows the use of an
105105
`aiohttp.ClientSession` object
106106
for all network requests. This
107107
session will not be closed by the library.
108108
If not provided, the library will open and
109109
close its own `ClientSession`s as needed.
110+
This session is also passed to the underlying
111+
socketio/engineio client to avoid blocking
112+
`ssl.create_default_context()` calls inside
113+
the event loop.
110114
111115
`connect_callback` - (Optional) A callback to be called when the
112116
Websocket connection is opened or reconnected after a network
@@ -193,7 +197,18 @@ async def sio_connect(self, director_bearer_token):
193197
# Disconnect previous sio object
194198
await self.sio_disconnect()
195199

196-
self._sio = socketio.AsyncClient(ssl_verify=False)
200+
if self.session is not None:
201+
# Create a new session using the caller's connector so engineio
202+
# can safely close it in _reset() without affecting the caller's
203+
# session.
204+
http_session = aiohttp.ClientSession(
205+
connector=self.session.connector, connector_owner=False
206+
)
207+
self._sio = socketio.AsyncClient(
208+
ssl_verify=False, http_session=http_session
209+
)
210+
else:
211+
self._sio = socketio.AsyncClient(ssl_verify=False)
197212
self._sio.register_namespace(
198213
_C4DirectorNamespace(
199214
token=director_bearer_token,

tests/test_websocket_ssl.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Tests for SSL context passthrough in C4Websocket."""
2+
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
import aiohttp
6+
import socketio_v4
7+
import pytest
8+
9+
from pyControl4.websocket import C4Websocket
10+
11+
12+
@pytest.mark.asyncio
13+
async def test_sio_connect_without_session():
14+
"""Test that sio_connect uses ssl_verify=False without http_session when
15+
no session is provided."""
16+
ws = C4Websocket("192.168.1.1")
17+
with patch.object(
18+
socketio_v4.AsyncClient, "__init__", return_value=None
19+
) as mock_init, patch.object(
20+
socketio_v4.AsyncClient, "register_namespace"
21+
), patch.object(
22+
socketio_v4.AsyncClient, "connect", new_callable=AsyncMock
23+
):
24+
await ws.sio_connect("test-token")
25+
mock_init.assert_called_once_with(ssl_verify=False)
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_sio_connect_with_session():
30+
"""Test that sio_connect creates a new session sharing the caller's
31+
connector and passes it as http_session, so engineio can safely close
32+
it without affecting the caller's session."""
33+
mock_connector = MagicMock()
34+
mock_session = MagicMock()
35+
mock_session.connector = mock_connector
36+
ws = C4Websocket("192.168.1.1", session_no_verify_ssl=mock_session)
37+
with patch.object(
38+
socketio_v4.AsyncClient, "__init__", return_value=None
39+
) as mock_init, patch.object(
40+
socketio_v4.AsyncClient, "register_namespace"
41+
), patch.object(
42+
socketio_v4.AsyncClient, "connect", new_callable=AsyncMock
43+
), patch.object(
44+
aiohttp, "ClientSession"
45+
) as mock_session_cls:
46+
mock_http_session = MagicMock()
47+
mock_session_cls.return_value = mock_http_session
48+
await ws.sio_connect("test-token")
49+
mock_session_cls.assert_called_once_with(
50+
connector=mock_connector, connector_owner=False
51+
)
52+
mock_init.assert_called_once_with(
53+
ssl_verify=False, http_session=mock_http_session
54+
)

0 commit comments

Comments
 (0)