-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e69b87c
commit b299653
Showing
14 changed files
with
270 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# This is a simple test to ensure we can make a websocket connection through a proxy server. It sets up a | ||
# simple server and a squid proxy server. The proxy server is inaccessible from the host machine, only the proxy | ||
# so we can confirm the proxy is actually working. | ||
|
||
name: Proxy Test | ||
on: | ||
pull_request: | ||
paths: | ||
- .github/workflows/proxy-test.yaml | ||
- scripts/proxy-test/* | ||
- "src/prefect/utilities/proxy.py" | ||
- requirements.txt | ||
- requirements-client.txt | ||
- requirements-dev.txt | ||
push: | ||
branches: | ||
- main | ||
paths: | ||
- .github/workflows/proxy-test.yaml | ||
- scripts/proxy-test/* | ||
- "src/prefect/utilities/proxy.py" | ||
- requirements.txt | ||
- requirements-client.txt | ||
- requirements-dev.txt | ||
|
||
jobs: | ||
proxy-test: | ||
name: Proxy Test | ||
timeout-minutes: 10 | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
persist-credentials: false | ||
fetch-depth: 0 | ||
|
||
- name: Set up Python 3.10 | ||
uses: actions/setup-python@v5 | ||
id: setup_python | ||
with: | ||
python-version: "3.10" | ||
|
||
- name: Create Docker networks | ||
run: | | ||
docker network create internal_net --internal | ||
docker network create external_net | ||
- name: Start API server container | ||
working-directory: scripts/proxy-test | ||
run: | | ||
docker build -t api-server . | ||
docker run -d --network internal_net --name server api-server | ||
- name: Start Squid Proxy container | ||
run: | | ||
docker run -d \ | ||
--network internal_net \ | ||
--network external_net \ | ||
-p 3128:3128 \ | ||
-v $(pwd)/scripts/proxy-test/squid.conf:/etc/squid/squid.conf \ | ||
--name proxy \ | ||
ubuntu/squid | ||
- name: Install Dependencies | ||
run: | | ||
python -m pip install -U uv | ||
uv pip install --upgrade --system . | ||
- name: Run Proxy Tests | ||
env: | ||
HTTP_PROXY: http://localhost:3128 | ||
HTTPS_PROXY: http://localhost:3128 | ||
run: python scripts/proxy-test/client.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
FROM python:3.11-slim | ||
|
||
WORKDIR /app | ||
|
||
COPY requirements.txt . | ||
RUN pip install uv | ||
RUN uv pip install --no-cache-dir --system -r requirements.txt | ||
|
||
COPY server.py . | ||
|
||
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
This is a simple test to ensure we can make a websocket connection through a proxy server. It sets up a | ||
simple server and a squid proxy server. The proxy server is inaccessible from the host machine, so we | ||
can confirm the proxy connection is working. | ||
|
||
``` | ||
$ uv pip install -r requirements.txt | ||
$ docker compose up --build | ||
$ python client.py | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import asyncio | ||
import os | ||
|
||
from prefect.utilities.proxy import websocket_connect | ||
|
||
PROXY_URL = "http://localhost:3128" | ||
WS_SERVER_URL = "ws://server:8000/ws" | ||
|
||
|
||
async def test_websocket_proxy_with_compat(): | ||
"""WebSocket through proxy with proxy compatibility code - should work""" | ||
os.environ["HTTP_PROXY"] = "http://localhost:3128" | ||
|
||
async with websocket_connect("ws://server:8000/ws") as websocket: | ||
await websocket.send("Hello!") | ||
response = await websocket.recv() | ||
print("Response: ", response) | ||
assert response == "Server received: Hello!" | ||
|
||
|
||
async def main(): | ||
print("Testing WebSocket through proxy with compatibility code") | ||
await test_websocket_proxy_with_compat() | ||
|
||
|
||
if __name__ == "__main__": | ||
asyncio.run(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
services: | ||
server: | ||
build: . | ||
networks: | ||
- internal_net | ||
|
||
forward_proxy: | ||
image: ubuntu/squid | ||
ports: | ||
- "3128:3128" | ||
volumes: | ||
- ./squid.conf:/etc/squid/squid.conf | ||
networks: | ||
- internal_net | ||
- external_net | ||
|
||
networks: | ||
internal_net: | ||
internal: true | ||
external_net: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
fastapi==0.111.1 | ||
uvicorn==0.28.1 | ||
uv==0.5.7 | ||
websockets==13.1 | ||
python-socks==2.5.3 | ||
httpx==0.28.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from fastapi import FastAPI, WebSocket | ||
|
||
app = FastAPI() | ||
|
||
|
||
@app.websocket("/ws") | ||
async def websocket_endpoint(websocket: WebSocket): | ||
await websocket.accept() | ||
async for data in websocket.iter_text(): | ||
await websocket.send_text(f"Server received: {data}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
http_port 3128 | ||
acl CONNECT method CONNECT | ||
acl SSL_ports port 443 8000 | ||
http_access allow CONNECT SSL_ports | ||
http_access allow all |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import os | ||
from typing import ( | ||
Any, | ||
Generator, | ||
) | ||
from urllib.parse import urlparse | ||
|
||
from python_socks.async_.asyncio import Proxy | ||
from typing_extensions import Self | ||
from websockets.client import WebSocketClientProtocol | ||
from websockets.legacy.client import Connect | ||
|
||
|
||
class WebsocketProxyConnect(Connect): | ||
def __init__(self: Self, uri: str, **kwargs: Any): | ||
# super() is intentionally deferred to the __proxy_connect__ method | ||
# to allow for the proxy to be established before the connection is made | ||
|
||
self.uri = uri | ||
self.__kwargs = kwargs | ||
|
||
u = urlparse(uri) | ||
host = u.hostname | ||
|
||
if u.scheme == "ws": | ||
port = u.port or 80 | ||
proxy_url = os.environ.get("HTTP_PROXY") | ||
elif u.scheme == "wss": | ||
port = u.port or 443 | ||
proxy_url = os.environ.get("HTTPS_PROXY") | ||
kwargs["server_hostname"] = host | ||
else: | ||
raise ValueError( | ||
"Unsupported scheme %s. Expected 'ws' or 'wss'. " % u.scheme | ||
) | ||
|
||
self.__proxy = Proxy.from_url(proxy_url) if proxy_url else None | ||
self.__host = host | ||
self.__port = port | ||
|
||
async def __proxy_connect__(self: Self) -> WebSocketClientProtocol: | ||
if self.__proxy: | ||
sock = await self.__proxy.connect( | ||
dest_host=self.__host, | ||
dest_port=self.__port, | ||
) | ||
self.__kwargs["sock"] = sock | ||
|
||
super().__init__(self.uri, **self.__kwargs) | ||
proto = await self.__await_impl__() | ||
return proto | ||
|
||
def __await__(self: Self) -> Generator[Any, None, WebSocketClientProtocol]: | ||
return self.__proxy_connect__().__await__() | ||
|
||
|
||
def websocket_connect(uri: str, **kwargs: Any) -> WebsocketProxyConnect: | ||
return WebsocketProxyConnect(uri, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from unittest.mock import Mock | ||
|
||
from prefect.utilities.proxy import WebsocketProxyConnect | ||
|
||
|
||
def test_init_ws_without_proxy(): | ||
client = WebsocketProxyConnect("ws://example.com") | ||
assert client.uri == "ws://example.com" | ||
assert client._WebsocketProxyConnect__host == "example.com" | ||
assert client._WebsocketProxyConnect__port == 80 | ||
assert client._WebsocketProxyConnect__proxy is None | ||
|
||
|
||
def test_init_wss_without_proxy(): | ||
client = WebsocketProxyConnect("wss://example.com") | ||
assert client.uri == "wss://example.com" | ||
assert client._WebsocketProxyConnect__host == "example.com" | ||
assert client._WebsocketProxyConnect__port == 443 | ||
assert "server_hostname" in client._WebsocketProxyConnect__kwargs | ||
assert client._WebsocketProxyConnect__proxy is None | ||
|
||
|
||
def test_init_ws_with_proxy(monkeypatch): | ||
monkeypatch.setenv("HTTP_PROXY", "http://proxy:3128") | ||
mock_proxy = Mock() | ||
monkeypatch.setattr("prefect.utilities.proxy.Proxy", mock_proxy) | ||
|
||
client = WebsocketProxyConnect("ws://example.com") | ||
|
||
mock_proxy.from_url.assert_called_once_with("http://proxy:3128") | ||
assert client._WebsocketProxyConnect__proxy is not None | ||
|
||
|
||
def test_init_wss_with_proxy(monkeypatch): | ||
monkeypatch.setenv("HTTPS_PROXY", "https://proxy:3128") | ||
mock_proxy = Mock() | ||
monkeypatch.setattr("prefect.utilities.proxy.Proxy", mock_proxy) | ||
|
||
client = WebsocketProxyConnect("wss://example.com") | ||
|
||
mock_proxy.from_url.assert_called_once_with("https://proxy:3128") | ||
assert client._WebsocketProxyConnect__proxy is not None |