summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2026-06-30 22:46:55 +0200
committerGravatar Tom van der Lee <tom@vanderlee.io>2026-06-30 22:46:55 +0200
commit12f2e24e2154113a6329d74aa556ae23506c34e1 (patch)
treee9c685e0a30f819f6b9e89a4f169cfe637dcbbd4
parentc4f33b3576e3a4a7f70b3d681fadae45f73ae31e (diff)
downloadserver-v3.tar.gz
server-v3.tar.bz2
server-v3.zip
WIPv3
-rw-r--r--.gitignore2
-rw-r--r--pyproject.toml4
-rw-r--r--ttun_server/__init__.py15
-rw-r--r--ttun_server/endpoints.py97
-rw-r--r--uv.lock500
5 files changed, 550 insertions, 68 deletions
diff --git a/.gitignore b/.gitignore
index a6d8f21..a9d7b09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -235,3 +235,5 @@ dmypy.json
235cython_debug/ 235cython_debug/
236 236
237# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all 237# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all
238
239.DS_Store
diff --git a/pyproject.toml b/pyproject.toml
index a3c3c03..70f2683 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ name = "ttun-server"
3version = "0.1.0" 3version = "0.1.0"
4requires-python = ">=3.14" 4requires-python = ">=3.14"
5dependencies = [ 5dependencies = [
6 "starlette~=1.0.0", 6 "fastapi[standard]>=0.138.2",
7 "uvicorn[standard]~=0.44.0",
8 "redis[hiredis]~=7.4.0", 7 "redis[hiredis]~=7.4.0",
8 "uvicorn[standard]~=0.44.0",
9] 9]
diff --git a/ttun_server/__init__.py b/ttun_server/__init__.py
index 2f8fed0..6c77858 100644
--- a/ttun_server/__init__.py
+++ b/ttun_server/__init__.py
@@ -1,28 +1,31 @@
1import logging 1import logging
2import os 2import os
3 3
4from starlette.applications import Starlette 4from fastapi import FastAPI
5from starlette.routing import Route, WebSocketRoute, Host, Router 5from starlette.routing import Host, Route, Router, WebSocketRoute
6 6
7from ttun_server.endpoints import Proxy, Health 7from ttun_server.endpoints import health, proxy
8from .websockets import WebsocketProxy, Tunnel 8from .websockets import WebsocketProxy, Tunnel
9 9
10logging.basicConfig(level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO'))) 10logging.basicConfig(level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO')))
11 11
12base_router = Router(routes=[ 12base_router = Router(routes=[
13 Route('/health/', Health), 13 Route('/health/', health),
14 WebSocketRoute('/tunnel/', Tunnel) 14 WebSocketRoute('/tunnel/', Tunnel)
15]) 15])
16 16
17server = Starlette( 17server = FastAPI(
18 debug=True, 18 debug=True,
19 routes=[ 19 routes=[
20 Host(os.environ['TUNNEL_DOMAIN'], base_router, 'base'), 20 Host(os.environ['TUNNEL_DOMAIN'], base_router, 'base'),
21 Route('/{path:path}', Proxy), 21 Route('/{path:path}', proxy),
22 WebSocketRoute('/{path:path}', WebsocketProxy) 22 WebSocketRoute('/{path:path}', WebsocketProxy)
23 ] 23 ]
24) 24)
25 25
26server.post()
27
28
26try: 29try:
27 from ._version import version 30 from ._version import version
28 __version__ = version 31 __version__ = version
diff --git a/ttun_server/endpoints.py b/ttun_server/endpoints.py
index fa5e7e7..22dcb6d 100644
--- a/ttun_server/endpoints.py
+++ b/ttun_server/endpoints.py
@@ -2,7 +2,7 @@ import logging
2from base64 import b64decode, b64encode 2from base64 import b64decode, b64encode
3from uuid import uuid4 3from uuid import uuid4
4 4
5from starlette.endpoints import HTTPEndpoint 5from starlette.background import BackgroundTask
6from starlette.requests import Request 6from starlette.requests import Request
7from starlette.responses import Response 7from starlette.responses import Response
8 8
@@ -12,62 +12,43 @@ from ttun_server.types import HttpRequestData, HttpMessageType, HttpMessage
12logger = logging.getLogger(__name__) 12logger = logging.getLogger(__name__)
13 13
14 14
15class HeaderMapping: 15async def proxy(request: Request) -> Response:
16 def __init__(self, headers: list[tuple[str, str]]): 16 [subdomain, *_] = request.headers['host'].split('.')
17 self._headers = headers 17 identifier = str(uuid4())
18 18 response_queue = await ProxyQueue.create_for_identifier(identifier)
19 def items(self): 19
20 for header in self._headers: 20 try:
21 yield header 21 request_queue = await ProxyQueue.get_for_identifier(subdomain)
22 22
23 23 logger.debug('PROXY %s%s ', subdomain, request.url)
24class Proxy(HTTPEndpoint): 24 await request_queue.enqueue(
25 async def dispatch(self) -> None: 25 HttpMessage(
26 request = Request(self.scope, self.receive) 26 type=HttpMessageType.request.value,
27 27 identifier=identifier,
28 [subdomain, *_] = request.headers['host'].split('.') 28 payload=HttpRequestData(
29 response = Response(content='Not Found', status_code=404) 29 method=request.method,
30 30 path=str(request.url).replace(str(request.base_url), '/'),
31 identifier = str(uuid4()) 31 headers=list(request.headers.items()),
32 response_queue = await ProxyQueue.create_for_identifier(identifier) 32 body=b64encode(await request.body()).decode()
33
34 try:
35
36 request_queue = await ProxyQueue.get_for_identifier(subdomain)
37
38 logger.debug('PROXY %s%s ', subdomain, request.url)
39 await request_queue.enqueue(
40 HttpMessage(
41 type=HttpMessageType.request.value,
42 identifier=identifier,
43 payload=
44 HttpRequestData(
45 method=request.method,
46 path=str(request.url).replace(str(request.base_url), '/'),
47 headers=list(request.headers.items()),
48 body=b64encode(await request.body()).decode()
49 )
50 ) 33 )
51 ) 34 )
52 35 )
53 _response = await response_queue.dequeue() 36
54 payload = _response['payload'] 37 _response = await response_queue.dequeue()
55 response = Response( 38 payload = _response['payload']
56 status_code=payload['status'], 39 return Response(
57 headers=HeaderMapping(payload['headers']), 40 status_code=payload['status'],
58 content=b64decode(payload['body'].encode()) 41 headers=dict(payload['headers']),
59 ) 42 content=b64decode(payload['body'].encode()),
60 except AssertionError: 43 background=BackgroundTask(response_queue.delete)
61 pass 44 )
62 finally: 45 except AssertionError:
63 await response(self.scope, self.receive, self.send) 46 return Response(
64 await response_queue.delete() 47 content='Not Found',
65 48 status_code=404,
66 49 background=BackgroundTask(response_queue.delete)
67class Health(HTTPEndpoint): 50 )
68 async def get(self, _) -> None: 51
69 response = Response(content='OK', status_code=200) 52
70 53async def health(_: Request) -> Response:
71 await response(self.scope, self.receive, self.send) 54 return Response(content='OK', status_code=200)
72
73
diff --git a/uv.lock b/uv.lock
index 8f15545..5688a0b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,6 +3,24 @@ revision = 3
3requires-python = ">=3.14" 3requires-python = ">=3.14"
4 4
5[[package]] 5[[package]]
6name = "annotated-doc"
7version = "0.0.4"
8source = { registry = "https://pypi.org/simple" }
9sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
10wheels = [
11 { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
12]
13
14[[package]]
15name = "annotated-types"
16version = "0.7.0"
17source = { registry = "https://pypi.org/simple" }
18sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
19wheels = [
20 { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
21]
22
23[[package]]
6name = "anyio" 24name = "anyio"
7version = "4.13.0" 25version = "4.13.0"
8source = { registry = "https://pypi.org/simple" } 26source = { registry = "https://pypi.org/simple" }
@@ -15,6 +33,15 @@ wheels = [
15] 33]
16 34
17[[package]] 35[[package]]
36name = "certifi"
37version = "2026.6.17"
38source = { registry = "https://pypi.org/simple" }
39sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
40wheels = [
41 { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
42]
43
44[[package]]
18name = "click" 45name = "click"
19version = "8.3.2" 46version = "8.3.2"
20source = { registry = "https://pypi.org/simple" } 47source = { registry = "https://pypi.org/simple" }
@@ -36,6 +63,146 @@ wheels = [
36] 63]
37 64
38[[package]] 65[[package]]
66name = "detect-installer"
67version = "0.1.0"
68source = { registry = "https://pypi.org/simple" }
69sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" }
70wheels = [
71 { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" },
72]
73
74[[package]]
75name = "dnspython"
76version = "2.8.0"
77source = { registry = "https://pypi.org/simple" }
78sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8