Support for WS

This commit is contained in:
JonatanRek 2025-02-26 08:55:49 +01:00
parent bbac6a5e2e
commit 30df8f46d1
5 changed files with 122 additions and 7 deletions

92
app.py
View File

@ -7,7 +7,10 @@ import time
from datetime import datetime, timezone from datetime import datetime, timezone
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
import os import os
import asyncio
import websockets
import hashlib
import base64
# Define the target server to proxy requests to # Define the target server to proxy requests to
class ProxyHandler(http.server.BaseHTTPRequestHandler): class ProxyHandler(http.server.BaseHTTPRequestHandler):
def __init__(self, configuration, docker_client): def __init__(self, configuration, docker_client):
@ -73,6 +76,15 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler):
return return
activity[proxy_host_configuration['domain']] = datetime.now(timezone.utc) activity[proxy_host_configuration['domain']] = datetime.now(timezone.utc)
# Check if this is a WebSocket request
if self.headers.get("Upgrade", "").lower() == "websocket":
print("Request is WS connecting to {0}".format(container['container_name']))
print("Request is WS connecting to {0}:{1}".format(proxy_host_configuration['proxy_host'],proxy_host_configuration['proxy_port']))
activity[proxy_host_configuration['domain']] = True
self.upgrade_to_websocket(proxy_host_configuration['proxy_host'], proxy_host_configuration['proxy_port'])
return
# Open a connection to the target server # Open a connection to the target server
conn = http.client.HTTPConnection(proxy_host_configuration['proxy_host'], proxy_host_configuration['proxy_port']) conn = http.client.HTTPConnection(proxy_host_configuration['proxy_host'], proxy_host_configuration['proxy_port'])
conn.request(method, self.path, headers=self.headers) conn.request(method, self.path, headers=self.headers)
@ -122,6 +134,56 @@ class ProxyHandler(http.server.BaseHTTPRequestHandler):
self.wfile.flush() self.wfile.flush()
async def websocket_proxy(self, target_host, target_port):
server_ws = None
try:
client_connection = self.connection
# Establish server connection to backend
server_ws = await websockets.connect(f"ws://{target_host}:{target_port}")
# Bridge function to handle message forwarding
async def bridge_websockets():
try:
while True:
# Wait for a message from the client
client_message = await client_connection.recv()
# Send it to the server
await server_ws.send(client_message)
# Wait for a message from the server
server_message = await server_ws.recv()
# Send it to the client
await client_connection.send(server_message)
except websockets.exceptions.ConnectionClosed as e:
print(f"WebSocket connection closed: {e}")
except Exception as e:
print(f"Error during WebSocket communication: {e}")
# Run the bridge coroutine
await bridge_websockets()
except Exception as e:
print(f"WebSocket proxy encountered an error: {e}")
finally:
if server_ws:
await server_ws.close()
def upgrade_to_websocket(self, target_host, target_port):
"""
Handles WebSocket upgrade requests and spawns an asyncio WebSocket proxy.
"""
key = self.headers['Sec-WebSocket-Key']
accept_val = base64.b64encode(hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')).digest()).decode('utf-8')
self.send_response(101) # Switching Protocols
self.send_header("Upgrade", "websocket")
self.send_header("Connection", "Upgrade")
self.send_header("Sec-WebSocket-Accept", accept_val)
self.end_headers()
loop = asyncio.new_event_loop()
threading.Thread(target=loop.run_until_complete, args=(self.websocket_proxy(target_host, target_port),)).start()
class ThreadedHTTPServer(ThreadingMixIn, http.server.HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, http.server.HTTPServer):
"""Handle requests in a separate thread.""" """Handle requests in a separate thread."""
@ -148,6 +210,9 @@ class BackgroundTasks(threading.Thread):
if (apps['domain'] in activity): if (apps['domain'] in activity):
dt = activity[apps['domain']] dt = activity[apps['domain']]
if (dt == True):
continue
diff_seconds = (datetime.now(timezone.utc) - dt).total_seconds() diff_seconds = (datetime.now(timezone.utc) - dt).total_seconds()
if(diff_seconds > apps['proxy_timeout_seconds']): if(diff_seconds > apps['proxy_timeout_seconds']):
print("stopping container: {0} ({1}) after {2}s".format(container['container_name'], container_object.id, diff_seconds)) print("stopping container: {0} ({1}) after {2}s".format(container['container_name'], container_object.id, diff_seconds))
@ -157,6 +222,30 @@ class BackgroundTasks(threading.Thread):
time.sleep(sleep_time) time.sleep(sleep_time)
async def websocket_proxy(client_ws, target_host, target_port):
"""
Forwards WebSocket messages between the client and the target container.
"""
try:
async with websockets.connect(f"ws://{target_host}:{target_port}") as server_ws:
# Create tasks to read from both directions
async def forward_client_to_server():
async for message in client_ws:
await server_ws.send(message)
async def forward_server_to_client():
async for message in server_ws:
await client_ws.send(message)
await asyncio.gather(forward_client_to_server(), forward_server_to_client())
except Exception as e:
print(f"WebSocket error: {e}")
finally:
await client_ws.close()
# MAIN #
if __name__ == '__main__': if __name__ == '__main__':
activity = {} activity = {}
@ -174,3 +263,4 @@ if __name__ == '__main__':
httpd = ThreadedHTTPServer(server_address, proxy_handler) httpd = ThreadedHTTPServer(server_address, proxy_handler)
print('Reverse proxy server running on port {0}...'.format(configuration['proxy_port'])) print('Reverse proxy server running on port {0}...'.format(configuration['proxy_port']))
httpd.serve_forever() httpd.serve_forever()

12
compose.yaml Normal file
View File

@ -0,0 +1,12 @@
services:
py_proxy:
build:
dockerfile: Dockerfile
context: .
restart: unless-stopped
volumes:
- ./config.yaml:/app/config.yaml
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 80:80
networks: {}

10
config copy.yml Normal file
View File

@ -0,0 +1,10 @@
proxy_port: 80
proxy_hosts:
- domain: wp.local
containers:
- container_name: wp-dev-db-1
- container_name: wp-dev-wordpress-1
proxy_host: localhost
proxy_port: 8888
proxy_timeout_seconds: 10
proxy_load_seconds: 5

View File

@ -1,10 +1,11 @@
proxy_port: 80 proxy_port: 80
proxy_hosts: proxy_hosts:
- domain: wp.local - domain: ws.local
containers: containers:
- container_name: wp-dev-db-1 - container_name: web-socket-test
- container_name: wp-dev-wordpress-1 - container_name: web-socket-test
proxy_host: localhost proxy_host: localhost
proxy_port: 8888 proxy_port: 8010
proxy_timeout_seconds: 10 proxy_timeout_seconds: 10
proxy_load_seconds: 5 proxy_load_seconds: 5

View File

@ -1 +1,3 @@
docker-py docker
PyYAML
websockets