aboutsummaryrefslogtreecommitdiffstats
path: root/proxy
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2026-09-04 17:09:24 +0200
committerGravatar Tom van der Lee <tom@vanderlee.io>2026-09-04 17:09:24 +0200
commit6b6f20a032eb31d747366c8bc270ca81731eaf6a (patch)
treeada48080495267bebde4ee1251bbbe77d629745f /proxy
parent2d22158dd95f76399f5716d556be60c77da04a46 (diff)
downloadserver-6b6f20a032eb31d747366c8bc270ca81731eaf6a.tar.gz
server-6b6f20a032eb31d747366c8bc270ca81731eaf6a.tar.bz2
server-6b6f20a032eb31d747366c8bc270ca81731eaf6a.zip
Fixed pre-commit issuesv3
Diffstat (limited to 'proxy')
-rw-r--r--proxy/__init__.py6
-rw-r--r--proxy/endpoints.py25
-rw-r--r--proxy/queue.py63
-rw-r--r--proxy/utils.py6
-rw-r--r--proxy/websockets.py48
5 files changed, 78 insertions, 70 deletions
diff --git a/proxy/__init__.py b/proxy/__init__.py
index fcdb23b..aba8ad2 100644
--- a/proxy/__init__.py
+++ b/proxy/__init__.py
@@ -7,7 +7,7 @@ from proxy.websockets import WebsocketProxy
7app = Starlette( 7app = Starlette(
8 debug=True, 8 debug=True,
9 routes=[ 9 routes=[
10 Route('/{path:path}', Proxy), 10 Route("/{path:path}", Proxy),
11 WebSocketRoute('/{path:path}', WebsocketProxy), 11 WebSocketRoute("/{path:path}", WebsocketProxy),
12 ] 12 ],
13) 13)
diff --git a/proxy/endpoints.py b/proxy/endpoints.py
index 0a23e7b..f514226 100644
--- a/proxy/endpoints.py
+++ b/proxy/endpoints.py
@@ -1,13 +1,13 @@
1import logging 1import logging
2from base64 import b64encode, b64decode 2from base64 import b64decode, b64encode
3from uuid import uuid4 3from uuid import uuid4
4 4
5from starlette.endpoints import HTTPEndpoint 5from starlette.endpoints import HTTPEndpoint
6from starlette.requests import Request 6from starlette.requests import Request
7from starlette.responses import Response 7from starlette.responses import Response
8 8
9from proxy.utils import get_path_with_query_string
10from proxy.queue import ProxyQueue 9from proxy.queue import ProxyQueue
10from proxy.utils import get_path_with_query_string
11from ttun_server.types import HttpMessage, HttpMessageType, HttpRequestData 11from ttun_server.types import HttpMessage, HttpMessageType, HttpRequestData
12 12
13logger = logging.getLogger(__name__) 13logger = logging.getLogger(__name__)
@@ -18,16 +18,15 @@ class HeaderMapping:
18 self._headers = headers 18 self._headers = headers
19 19
20 def items(self): 20 def items(self):
21 for header in self._headers: 21 yield from self._headers
22 yield header
23 22
24 23
25class Proxy(HTTPEndpoint): 24class Proxy(HTTPEndpoint):
26 async def dispatch(self) -> None: 25 async def dispatch(self) -> None:
27 request = Request(self.scope, self.receive) 26 request = Request(self.scope, self.receive)
28 27
29 subdomain = request.path_params['subdomain'] 28 subdomain = request.path_params["subdomain"]
30 response = Response(content='Not Found', status_code=404) 29 response = Response(content="Not Found", status_code=404)
31 30
32 identifier = str(uuid4()) 31 identifier = str(uuid4())
33 response_queue = await ProxyQueue.create_for_identifier(identifier) 32 response_queue = await ProxyQueue.create_for_identifier(identifier)
@@ -35,7 +34,7 @@ class Proxy(HTTPEndpoint):
35 try: 34 try:
36 request_queue = await ProxyQueue.get_for_identifier(subdomain) 35 request_queue = await ProxyQueue.get_for_identifier(subdomain)
37 36
38 logger.debug('PROXY %s%s ', subdomain, request.url) 37 logger.debug("PROXY %s%s ", subdomain, request.url)
39 await request_queue.enqueue( 38 await request_queue.enqueue(
40 HttpMessage( 39 HttpMessage(
41 type=HttpMessageType.request.value, 40 type=HttpMessageType.request.value,
@@ -44,17 +43,17 @@ class Proxy(HTTPEndpoint):
44 method=request.method, 43 method=request.method,
45 path=get_path_with_query_string(request), 44 path=get_path_with_query_string(request),
46 headers=list(request.headers.items()), 45 headers=list(request.headers.items()),
47 body=b64encode(await request.body()).decode() 46 body=b64encode(await request.body()).decode(),
48 ) 47 ),
49 ) 48 )
50 ) 49 )
51 50
52 _response = await response_queue.dequeue() 51 _response = await response_queue.dequeue()
53 payload = _response['payload'] 52 payload = _response["payload"]
54 response = Response( 53 response = Response(
55 status_code=payload['status'], 54 status_code=payload["status"],
56 headers=HeaderMapping(payload['headers']), 55 headers=HeaderMapping(payload["headers"]),
57 content=b64decode(payload['body'].encode()) 56 content=b64decode(payload["body"].encode()),
58 ) 57 )
59 except AssertionError: 58 except AssertionError:
60 pass 59 pass
diff --git a/proxy/queue.py b/proxy/queue.py
index cfa0f3c..70fe3d0 100644
--- a/proxy/queue.py
+++ b/proxy/queue.py
@@ -2,8 +2,7 @@ import asyncio
2import json 2import json
3import logging 3import logging
4import os 4import os
5import traceback 5from typing import ClassVar, Self
6from typing import Type
7 6
8from ttun_server.redis import RedisConnectionPool 7from ttun_server.redis import RedisConnectionPool
9from ttun_server.types import Message 8from ttun_server.types import Message
@@ -16,37 +15,37 @@ class BaseProxyQueue:
16 self.identifier = identifier 15 self.identifier = identifier
17 16
18 @classmethod 17 @classmethod
19 async def create_for_identifier(cls, identifier: str) -> 'BaseProxyQueue': 18 async def create_for_identifier(cls, identifier: str) -> BaseProxyQueue:
20 raise NotImplementedError(f'Please implement create_for_identifier') 19 raise NotImplementedError("Please implement create_for_identifier")
21 20
22 @classmethod 21 @classmethod
23 async def get_for_identifier(cls, identifier: str) -> Type['self']: 22 async def get_for_identifier(cls, identifier: str) -> Self:
24 assert await cls.has_connection(identifier) 23 assert await cls.has_connection(identifier)
25 return cls(identifier) 24 return cls(identifier)
26 25
27 @classmethod 26 @classmethod
28 async def has_connection(cls, identifier) -> bool: 27 async def has_connection(cls, identifier) -> bool:
29 raise NotImplementedError(f'Please implement has_connection') 28 raise NotImplementedError("Please implement has_connection")
30 29
31 async def enqueue(self, message: Message): 30 async def enqueue(self, message: Message):
32 raise NotImplementedError(f'Please implement send_request') 31 raise NotImplementedError("Please implement send_request")
33 32
34 async def dequeue(self) -> Message: 33 async def dequeue(self) -> Message:
35 raise NotImplementedError(f'Please implement handle_requests') 34 raise NotImplementedError("Please implement handle_requests")
36 35
37 async def delete(self): 36 async def delete(self):
38 raise NotImplementedError(f'Please implement delete') 37 raise NotImplementedError("Please implement delete")
39 38
40 39
41class MemoryProxyQueue(BaseProxyQueue): 40class MemoryProxyQueue(BaseProxyQueue):
42 connections: dict[str, asyncio.Queue] = {} 41 connections: ClassVar[dict[str, asyncio.Queue]] = {}
43 42
44 @classmethod 43 @classmethod
45 async def has_connection(cls, identifier) -> bool: 44 async def has_connection(cls, identifier) -> bool:
46 return identifier in cls.connections 45 return identifier in cls.connections
47 46
48 @classmethod 47 @classmethod
49 async def create_for_identifier(cls, identifier: str) -> 'MemoryProxyQueue': 48 async def create_for_identifier(cls, identifier: str) -> MemoryProxyQueue:
50 instance = cls(identifier) 49 instance = cls(identifier)
51 cls.connections[identifier] = asyncio.Queue() 50 cls.connections[identifier] = asyncio.Queue()
52 51
@@ -66,67 +65,59 @@ class RedisProxyQueue(BaseProxyQueue):
66 def __init__(self, identifier): 65 def __init__(self, identifier):
67 super().__init__(identifier) 66 super().__init__(identifier)
68 67
69 self.pubsub = RedisConnectionPool()\ 68 self.pubsub = RedisConnectionPool().get_connection().pubsub()
70 .get_connection()\
71 .pubsub()
72 69
73 self.subscription_queue = asyncio.Queue() 70 self.subscription_queue = asyncio.Queue()
74 71
75 @classmethod 72 @classmethod
76 async def create_for_identifier(cls, identifier: str) -> 'BaseProxyQueue': 73 async def create_for_identifier(cls, identifier: str) -> BaseProxyQueue:
77 instance = cls(identifier) 74 instance = cls(identifier)
78 75
79 await instance.pubsub.subscribe(f'request_{identifier}') 76 await instance.pubsub.subscribe(f"request_{identifier}")
80 return instance 77 return instance
81 78
82 @classmethod 79 @classmethod
83 async def get_for_identifier(cls, identifier: str) -> 'RedisProxyQueue': 80 async def get_for_identifier(cls, identifier: str) -> RedisProxyQueue:
84 instance: 'RedisProxyQueue' = await super().get_for_identifier(identifier) 81 instance: RedisProxyQueue = await super().get_for_identifier(identifier)
85 82
86 await instance.pubsub.subscribe(f'response_{identifier}') 83 await instance.pubsub.subscribe(f"response_{identifier}")
87 84
88 return instance 85 return instance
89 86
90 @classmethod 87 @classmethod
91 async def has_connection(cls, identifier) -> bool: 88 async def has_connection(cls, identifier) -> bool:
92 logger.debug(await RedisConnectionPool.get_connection().pubsub_channels()) 89 logger.debug(await RedisConnectionPool.get_connection().pubsub_channels())
93 return f'request_{identifier}' in { 90 return f"request_{identifier}" in {
94 channel.decode() 91 channel.decode()
95 for channel 92 for channel in await RedisConnectionPool.get_connection().pubsub_channels()
96 in await RedisConnectionPool \
97 .get_connection() \
98 .pubsub_channels()
99 } 93 }
100 94
101 async def wait_for_message(self): 95 async def wait_for_message(self):
102 async for message in self.pubsub.listen(): 96 async for message in self.pubsub.listen():
103 match message['type']: 97 match message["type"]:
104 case 'subscribe': 98 case "subscribe":
105 continue 99 continue
106 case _: 100 case _:
107 return message['data'] 101 return message["data"]
108 102
109 async def enqueue(self, message: Message): 103 async def enqueue(self, message: Message):
110 await RedisConnectionPool \ 104 await RedisConnectionPool.get_connection().publish(
111 .get_connection() \ 105 f"request_{self.identifier}", json.dumps(message)
112 .publish(f'request_{self.identifier}', json.dumps(message)) 106 )
113 107
114 async def dequeue(self) -> Message: 108 async def dequeue(self) -> Message:
115 message = await self.wait_for_message() 109 message = await self.wait_for_message()
116 return json.loads(message) 110 return json.loads(message)
117 111
118 async def delete(self): 112 async def delete(self):
119 await self.pubsub.unsubscribe(f'request_{self.identifier}') 113 await self.pubsub.unsubscribe(f"request_{self.identifier}")
120 114
121 await RedisConnectionPool.get_connection()\ 115 await RedisConnectionPool.get_connection().srem("connections", self.identifier)
122 .srem('connections', self.identifier)
123 116
124 117
125class ProxyQueueMeta(type): 118class ProxyQueueMeta(type):
126 def __new__(cls, name, superclasses, attributes): 119 def __new__(cls, name, superclasses, attributes):
127 return RedisProxyQueue \ 120 return RedisProxyQueue if "REDIS_URL" in os.environ else MemoryProxyQueue
128 if 'REDIS_URL' in os.environ \
129 else MemoryProxyQueue
130 121
131 122
132class ProxyQueue(BaseProxyQueue, metaclass=ProxyQueueMeta): 123class ProxyQueue(BaseProxyQueue, metaclass=ProxyQueueMeta):
diff --git a/proxy/utils.py b/proxy/utils.py
index 2b80f43..3fff388 100644
--- a/proxy/utils.py
+++ b/proxy/utils.py
@@ -3,5 +3,9 @@ from starlette.requests import HTTPConnection
3 3
4def get_path_with_query_string(connection: HTTPConnection) -> str: 4def get_path_with_query_string(connection: HTTPConnection) -> str:
5 path = connection.url.path 5 path = connection.url.path
6 query_string = '?' + connection.scope['query_string'].decode() if connection.scope['query_string'] else '' 6 query_string = (
7 "?" + connection.scope["query_string"].decode()
8 if connection.scope["query_string"]
9 else ""
10 )
7 return f"{path}{query_string}" 11 return f"{path}{query_string}"
diff --git a/proxy/websockets.py b/proxy/websockets.py
index ec65e6d..a09e261 100644
--- a/proxy/websockets.py
+++ b/proxy/websockets.py
@@ -9,17 +9,22 @@ from uuid import uuid4
9from starlette.endpoints import WebSocketEndpoint 9from starlette.endpoints import WebSocketEndpoint
10from starlette.websockets import WebSocket 10from starlette.websockets import WebSocket
11 11
12from proxy.utils import get_path_with_query_string
13from proxy.queue import ProxyQueue 12from proxy.queue import ProxyQueue
14from ttun_server.types import WebsocketMessage, WebsocketMessageType, WebsocketConnectData, WebsocketMessageData, \ 13from proxy.utils import get_path_with_query_string
15 WebsocketDisconnectData 14from ttun_server.types import (
15 WebsocketConnectData,
16 WebsocketDisconnectData,
17 WebsocketMessage,
18 WebsocketMessageData,
19 WebsocketMessageType,
20)
16 21
17logger = logging.getLogger(__name__) 22logger = logging.getLogger(__name__)
18logger.setLevel('DEBUG') 23logger.setLevel("DEBUG")
19 24
20 25
21class WebsocketProxy(WebSocketEndpoint): 26class WebsocketProxy(WebSocketEndpoint):
22 encoding = 'json' 27 encoding = "json"
23 websocket_listen_task = None 28 websocket_listen_task = None
24 29
25 def __init__(self, *args, **kwargs): 30 def __init__(self, *args, **kwargs):
@@ -28,16 +33,20 @@ class WebsocketProxy(WebSocketEndpoint):
28 33
29 @asynccontextmanager 34 @asynccontextmanager
30 async def proxy(self, websocket: WebSocket, message: WebsocketMessage): 35 async def proxy(self, websocket: WebSocket, message: WebsocketMessage):
31 [subdomain, *_] = websocket.url.hostname.split('.') 36 [subdomain, *_] = websocket.url.hostname.split(".")
32 37
33 expect_ack = WebsocketMessageType(message['type']) == WebsocketMessageType.connect 38 expect_ack = (
39 WebsocketMessageType(message["type"]) == WebsocketMessageType.connect
40 )
34 41
35 try: 42 try:
36 request_queue = await ProxyQueue.get_for_identifier(subdomain) 43 request_queue = await ProxyQueue.get_for_identifier(subdomain)
37 await request_queue.enqueue(message) 44 await request_queue.enqueue(message)
38 45
39 if expect_ack: 46 if expect_ack:
40 response_queue = await ProxyQueue.create_for_identifier(message["identifier"]) 47 response_queue = await ProxyQueue.create_for_identifier(
48 message["identifier"]
49 )
41 yield await response_queue.dequeue() 50 yield await response_queue.dequeue()
42 await response_queue.delete() 51 await response_queue.delete()
43 else: 52 else:
@@ -51,7 +60,9 @@ class WebsocketProxy(WebSocketEndpoint):
51 while True: 60 while True:
52 message: WebsocketMessage = await response_queue.dequeue() 61 message: WebsocketMessage = await response_queue.dequeue()
53 logger.debug(message) 62 logger.debug(message)
54 await websocket.send_text(b64decode(message['payload']['body'].encode()).decode()) 63 await websocket.send_text(
64 b64decode(message["payload"]["body"].encode()).decode()
65 )
55 66
56 async def on_connect(self, websocket: WebSocket) -> None: 67 async def on_connect(self, websocket: WebSocket) -> None:
57 message = WebsocketMessage( 68 message = WebsocketMessage(
@@ -60,18 +71,21 @@ class WebsocketProxy(WebSocketEndpoint):
60 payload=WebsocketConnectData( 71 payload=WebsocketConnectData(
61 path=get_path_with_query_string(websocket), 72 path=get_path_with_query_string(websocket),
62 headers=[ 73 headers=[
63 (k.decode(), v.decode()) 74 (k.decode(), v.decode()) for k, v in websocket.scope["headers"]
64 for k, v
65 in websocket.scope['headers']
66 ], 75 ],
67 ) 76 ),
68 ) 77 )
69 78
70 async with self.proxy(websocket, message) as m: 79 async with self.proxy(websocket, message) as m:
71 if m is not None and WebsocketMessageType(m['type']) == WebsocketMessageType.ack: 80 if (
81 m is not None
82 and WebsocketMessageType(m["type"]) == WebsocketMessageType.ack
83 ):
72 await super().on_connect(websocket) 84 await super().on_connect(websocket)
73 85
74 self.websocket_listen_task = asyncio.create_task(self.listen_for_messages(websocket)) 86 self.websocket_listen_task = asyncio.create_task(
87 self.listen_for_messages(websocket)
88 )
75 89
76 def callback(*args, **kwargs): 90 def callback(*args, **kwargs):
77 self.websocket_listen_task = None 91 self.websocket_listen_task = None
@@ -92,7 +106,7 @@ class WebsocketProxy(WebSocketEndpoint):
92 identifier=self.id, 106 identifier=self.id,
93 payload=WebsocketMessageData( 107 payload=WebsocketMessageData(
94 body=b64encode(data_bytes).decode(), 108 body=b64encode(data_bytes).decode(),
95 ) 109 ),
96 ) 110 )
97 111
98 async with self.proxy(websocket, message): 112 async with self.proxy(websocket, message):
@@ -104,7 +118,7 @@ class WebsocketProxy(WebSocketEndpoint):
104 identifier=self.id, 118 identifier=self.id,
105 payload=WebsocketDisconnectData( 119 payload=WebsocketDisconnectData(
106 close_code=close_code, 120 close_code=close_code,
107 ) 121 ),
108 ) 122 )
109 123
110 async with self.proxy(websocket, message): 124 async with self.proxy(websocket, message):