summaryrefslogtreecommitdiffstats
path: root/ttun_server
diff options
context:
space:
mode:
Diffstat (limited to 'ttun_server')
-rw-r--r--ttun_server/__init__.py12
-rw-r--r--ttun_server/connections.py3
-rw-r--r--ttun_server/endpoints.py98
-rw-r--r--ttun_server/types.py25
4 files changed, 138 insertions, 0 deletions
diff --git a/ttun_server/__init__.py b/ttun_server/__init__.py
new file mode 100644
index 0000000..b8fd114
--- /dev/null
+++ b/ttun_server/__init__.py
@@ -0,0 +1,12 @@
1from starlette.applications import Starlette
2from starlette.routing import Route, WebSocketRoute
3
4from ttun_server.endpoints import Proxy, Tunnel
5
6server = Starlette(
7 debug=True,
8 routes=[
9 Route('/{path:path}', Proxy),
10 WebSocketRoute('/tunnel/', Tunnel)
11 ]
12)
diff --git a/ttun_server/connections.py b/ttun_server/connections.py
new file mode 100644
index 0000000..a8dabcf
--- /dev/null
+++ b/ttun_server/connections.py
@@ -0,0 +1,3 @@
1from ttun_server.types import Connection
2
3connections: dict[str, Connection] = {}
diff --git a/ttun_server/endpoints.py b/ttun_server/endpoints.py
new file mode 100644
index 0000000..d59cb7c
--- /dev/null
+++ b/ttun_server/endpoints.py
@@ -0,0 +1,98 @@
1import asyncio
2import os
3from asyncio import Queue
4from base64 import b64decode, b64encode
5from typing import Optional, Any
6from uuid import uuid4
7
8from starlette.endpoints import HTTPEndpoint, WebSocketEndpoint
9from starlette.requests import Request
10from starlette.responses import Response
11from starlette.types import Scope, Receive, Send
12from starlette.websockets import WebSocket
13
14from ttun_server.types import Connection, RequestData, Config, ResponseData
15
16from ttun_server.connections import connections
17
18
19class Proxy(HTTPEndpoint):
20 async def dispatch(self) -> None:
21 request = Request(self.scope, self.receive)
22
23 [subdomain, *_] = request.headers['host'].split('.')
24 response = Response(content='Not Found', status_code=404)
25
26 if subdomain in connections:
27 connection = connections[subdomain]
28
29 await connection['requests'].put(RequestData(
30 method=request.method,
31 path=str(request.url).replace(str(request.base_url), '/'),
32 headers=dict(request.headers),
33 cookies=dict(request.cookies),
34 body=b64encode(await request.body()).decode()
35 ))
36
37 _response = await connection['responses'].get()
38 response = Response(
39 status_code=_response['status'],
40 headers=_response['headers'],
41 content=b64decode(_response['body'].encode())
42 )
43
44 await response(self.scope, self.receive, self.send)
45
46
47class Tunnel(WebSocketEndpoint):
48 encoding = 'json'
49
50 def __init__(self, scope: Scope, receive: Receive, send: Send):
51 super().__init__(scope, receive, send)
52 self.request_task = None
53 self.config: Optional[Config] = None
54
55 @property
56 def requests(self) -> Queue[RequestData]:
57 return connections[self.config['subdomain']]['requests']
58
59 @property
60 def responses(self) -> Queue[ResponseData]:
61 return connections[self.config['subdomain']]['responses']
62
63 async def handle_requests(self, websocket: WebSocket):
64 while request := await self.requests.get():
65 await websocket.send_json(request)
66
67 async def on_connect(self, websocket: WebSocket) -> None:
68 await websocket.accept()
69 self.config = await websocket.receive_json()
70
71 if self.config['subdomain'] is None \
72 or self.config['subdomain'] in connections:
73 self.config['subdomain'] = uuid4().hex
74
75
76 connections[self.config['subdomain']] = Connection(
77 requests=Queue(),
78 responses=Queue(),
79 )
80
81 hostname = os.environ.get("TUNNEL_DOMAIN")
82 protocol = "https" if os.environ.get("SECURE", False) else "http"
83
84 await websocket.send_json({
85 'url': f'{protocol}://{self.config["subdomain"]}.{hostname}'
86 })
87
88 self.request_task = asyncio.create_task(self.handle_requests(websocket))
89
90 async def on_receive(self, websocket: WebSocket, data: Any) -> None:
91 await self.responses.put(data)
92
93 async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
94 if self.config is not None and self.config['subdomain'] in connections:
95 del connections[self.config['subdomain']]
96
97 if self.request_task is not None:
98 self.request_task.cancel()
diff --git a/ttun_server/types.py b/ttun_server/types.py
new file mode 100644
index 0000000..0b2fb87
--- /dev/null
+++ b/ttun_server/types.py
@@ -0,0 +1,25 @@
1from asyncio import Queue
2from typing import TypedDict, Optional
3
4
5class Config(TypedDict):
6 subdomain: str
7
8class RequestData(TypedDict):
9 method: str
10 path: str
11 headers: dict
12 cookies: dict
13 body: Optional[str]
14
15
16class ResponseData(TypedDict):
17 status: int
18 headers: dict
19 cookies: dict
20 body: Optional[str]
21
22
23class Connection(TypedDict):
24 requests: Queue[RequestData]
25 responses: Queue[ResponseData]