mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-06 13:30:41 +02:00
feat: adds support for services behind a SOCKS5 proxy
This commit is contained in:
parent
3d65240784
commit
18c66a8318
@ -6,6 +6,8 @@ import urllib.parse
|
|||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import aiohttp_socks.connector
|
||||||
import socketio # type: ignore
|
import socketio # type: ignore
|
||||||
|
|
||||||
from pydase.client.proxy_class import ProxyClass
|
from pydase.client.proxy_class import ProxyClass
|
||||||
@ -40,47 +42,52 @@ def asyncio_loop_thread(loop: asyncio.AbstractEventLoop) -> None:
|
|||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
"""
|
"""A client for connecting to a remote pydase service using Socket.IO. This client
|
||||||
A client for connecting to a remote pydase service using socket.io. This client
|
|
||||||
handles asynchronous communication with a service, manages events such as
|
handles asynchronous communication with a service, manages events such as
|
||||||
connection, disconnection, and updates, and ensures that the proxy object is
|
connection, disconnection, and updates, and ensures that the proxy object is
|
||||||
up-to-date with the server state.
|
up-to-date with the server state.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url:
|
url: The URL of the pydase Socket.IO server. This should always contain the
|
||||||
The URL of the pydase Socket.IO server. This should always contain the
|
protocol (e.g., `ws` or `wss`) and the hostname, and can optionally include
|
||||||
protocol and the hostname.
|
a path prefix (e.g., `ws://localhost:8001/service`).
|
||||||
block_until_connected:
|
block_until_connected: If set to True, the constructor will block until the
|
||||||
If set to True, the constructor will block until the connection to the
|
connection to the service has been established. This is useful for ensuring
|
||||||
service has been established. This is useful for ensuring the client is
|
the client is ready to use immediately after instantiation. Default is True.
|
||||||
ready to use immediately after instantiation. Default is True.
|
sio_client_kwargs: Additional keyword arguments passed to the underlying
|
||||||
sio_client_kwargs:
|
|
||||||
Additional keyword arguments passed to the underlying
|
|
||||||
[`AsyncClient`][socketio.AsyncClient]. This allows fine-tuning of the
|
[`AsyncClient`][socketio.AsyncClient]. This allows fine-tuning of the
|
||||||
client's behaviour (e.g., reconnection attempts or reconnection delay).
|
client's behaviour (e.g., reconnection attempts or reconnection delay).
|
||||||
Default is an empty dictionary.
|
client_id: An optional client identifier. This ID is sent to the server as the
|
||||||
client_id: Client identification that will be shown in the server logs this
|
`X-Client-Id` HTTP header. It can be used for logging or authentication
|
||||||
client is connecting to. This ID is passed as a `X-Client-Id` header in the
|
purposes on the server side.
|
||||||
HTTP(s) request. Defaults to None.
|
proxy_url: An optional proxy URL to route the connection through. This is useful
|
||||||
|
if the service is only reachable via an SSH tunnel or behind a firewall
|
||||||
|
(e.g., `socks5://localhost:2222`).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
The following example demonstrates a `Client` instance that connects to another
|
Connect to a service directly:
|
||||||
pydase service, while customising some of the connection settings for the
|
|
||||||
underlying [`AsyncClient`][socketio.AsyncClient].
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
pydase.Client(url="ws://localhost:8001", sio_client_kwargs={
|
client = pydase.Client(url="ws://localhost:8001")
|
||||||
"reconnection_attempts": 2,
|
|
||||||
"reconnection_delay": 2,
|
|
||||||
"reconnection_delay_max": 8,
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When connecting to a server over a secure connection (i.e., the server is using
|
Connect over a secure connection:
|
||||||
SSL/TLS encryption), make sure that the `wss` protocol is used instead of `ws`:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
pydase.Client(url="wss://my-service.example.com")
|
client = pydase.Client(url="wss://my-service.example.com")
|
||||||
|
```
|
||||||
|
|
||||||
|
Connect using a SOCKS5 proxy (e.g., through an SSH tunnel):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -D 2222 user@gateway.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = pydase.Client(
|
||||||
|
url="ws://remote-server:8001",
|
||||||
|
proxy_url="socks5://localhost:2222"
|
||||||
|
)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -91,6 +98,7 @@ class Client:
|
|||||||
block_until_connected: bool = True,
|
block_until_connected: bool = True,
|
||||||
sio_client_kwargs: dict[str, Any] = {},
|
sio_client_kwargs: dict[str, Any] = {},
|
||||||
client_id: str | None = None,
|
client_id: str | None = None,
|
||||||
|
proxy_url: str | None = None,
|
||||||
):
|
):
|
||||||
# Parse the URL to separate base URL and path prefix
|
# Parse the URL to separate base URL and path prefix
|
||||||
parsed_url = urllib.parse.urlparse(url)
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
@ -103,8 +111,9 @@ class Client:
|
|||||||
# Store the path prefix (e.g., "/service" in "ws://localhost:8081/service")
|
# Store the path prefix (e.g., "/service" in "ws://localhost:8081/service")
|
||||||
self._path_prefix = parsed_url.path.rstrip("/") # Remove trailing slash if any
|
self._path_prefix = parsed_url.path.rstrip("/") # Remove trailing slash if any
|
||||||
self._url = url
|
self._url = url
|
||||||
self._sio = socketio.AsyncClient(**sio_client_kwargs)
|
self._proxy_url = proxy_url
|
||||||
self._client_id = client_id
|
self._client_id = client_id
|
||||||
|
self._sio_client_kwargs = sio_client_kwargs
|
||||||
self._loop: asyncio.AbstractEventLoop | None = None
|
self._loop: asyncio.AbstractEventLoop | None = None
|
||||||
self._thread: threading.Thread | None = None
|
self._thread: threading.Thread | None = None
|
||||||
self.proxy: ProxyClass
|
self.proxy: ProxyClass
|
||||||
@ -125,6 +134,19 @@ class Client:
|
|||||||
|
|
||||||
def connect(self, block_until_connected: bool = True) -> None:
|
def connect(self, block_until_connected: bool = True) -> None:
|
||||||
if self._thread is None or self._loop is None:
|
if self._thread is None or self._loop is None:
|
||||||
|
if self._proxy_url is not None:
|
||||||
|
session = aiohttp.ClientSession(
|
||||||
|
connector=aiohttp_socks.connector.ProxyConnector.from_url(
|
||||||
|
url=self._proxy_url, loop=self._loop
|
||||||
|
),
|
||||||
|
loop=self._loop,
|
||||||
|
)
|
||||||
|
self._sio = socketio.AsyncClient(
|
||||||
|
http_session=session, **self._sio_client_kwargs
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._sio = socketio.AsyncClient(**self._sio_client_kwargs)
|
||||||
|
|
||||||
self._loop = self._initialize_loop_and_thread()
|
self._loop = self._initialize_loop_and_thread()
|
||||||
|
|
||||||
connection_future = asyncio.run_coroutine_threadsafe(
|
connection_future = asyncio.run_coroutine_threadsafe(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user