aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2021-12-30 10:16:41 +0100
committerGravatar Tom van der Lee <tom@vanderlee.io>2022-01-09 13:00:48 +0100
commit46af86f8ace136dd1d1d94590d3423e6b12e3f7b (patch)
treef190663cd8202c7bc7034adbf9dabd7da293082c
downloadserver-46af86f8ace136dd1d1d94590d3423e6b12e3f7b.tar.gz
server-46af86f8ace136dd1d1d94590d3423e6b12e3f7b.tar.bz2
server-46af86f8ace136dd1d1d94590d3423e6b12e3f7b.zip
Prepare for githubv1.0.0
-rw-r--r--.dockerignore1
-rw-r--r--.github/workflows/docker-image.yml39
-rw-r--r--.gitignore237
-rw-r--r--.python-version1
-rw-r--r--Dockerfile24
-rw-r--r--LICENSE29
-rw-r--r--README.rst41
-rw-r--r--requirements.txt2
-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
12 files changed, 512 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..61f2dc9
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
**/__pycache__/
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
new file mode 100644
index 0000000..8945bfb
--- /dev/null
+++ b/.github/workflows/docker-image.yml
@@ -0,0 +1,39 @@
1name: Release
2
3on:
4 push:
5 tags:
6 - 'v*'
7 pull_request:
8 branches:
9 - 'main'
10
11jobs:
12 docker:
13 runs-on: ubuntu-latest
14 steps:
15 - name: Checkout
16 uses: actions/checkout@v2
17 - name: Docker meta
18 id: meta
19 uses: docker/metadata-action@v3
20 with:
21 images: ghcr.io/tomvanderlee/ttun-server
22 tags: |
23 type=semver,pattern={{major}}
24 type=semver,pattern={{major}}.{{minor}}
25
26 - name: Login to DockerHub
27 if: github.event_name != 'pull_request'
28 uses: docker/login-action@v1
29 with:
30 registry: ghcr.io
31 username: ${{ github.actor }}
32 password: ${{ secrets.GITHUB_TOKEN }}
33 - name: Build and push
34 uses: docker/build-push-action@v2
35 with:
36 context: .
37 push: ${{ github.event_name != 'pull_request' }}
38 tags: ${{ steps.meta.outputs.tags }}
39 labels: ${{ steps.meta.outputs.labels }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a6d8f21
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,237 @@
1
2# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all
3# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all
4
5### PyCharm+all ###
6# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
7# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8
9# User-specific stuff
10.idea/**/workspace.xml
11.idea/**/tasks.xml
12.idea/**/usage.statistics.xml
13.idea/**/dictionaries
14.idea/**/shelf
15
16# AWS User-specific
17.idea/**/aws.xml
18
19# Generated files
20.idea/**/contentModel.xml
21
22# Sensitive or high-churn files
23.idea/**/dataSources/
24.idea/**/dataSources.ids
25.idea/**/dataSources.local.xml
26.idea/**/sqlDataSources.xml
27.idea/**/dynamic.xml
28.idea/**/uiDesigner.xml
29.idea/**/dbnavigator.xml
30
31# Gradle
32.idea/**/gradle.xml
33.idea/**/libraries
34
35# Gradle and Maven with auto-import
36# When using Gradle or Maven with auto-import, you should exclude module files,
37# since they will be recreated, and may cause churn. Uncomment if using
38# auto-import.
39# .idea/artifacts
40# .idea/compiler.xml
41# .idea/jarRepositories.xml
42# .idea/modules.xml
43# .idea/*.iml
44# .idea/modules
45# *.iml
46# *.ipr
47
48# CMake
49cmake-build-*/
50
51# Mongo Explorer plugin
52.idea/**/mongoSettings.xml
53
54# File-based project format
55*.iws
56
57# IntelliJ
58out/
59
60# mpeltonen/sbt-idea plugin
61.idea_modules/
62
63# JIRA plugin
64atlassian-ide-plugin.xml
65
66# Cursive Clojure plugin
67.idea/replstate.xml
68
69# Crashlytics plugin (for Android Studio and IntelliJ)
70com_crashlytics_export_strings.xml
71crashlytics.properties
72crashlytics-build.properties
73fabric.properties
74
75# Editor-based Rest Client
76.idea/httpRequests
77
78# Android studio 3.1+ serialized cache file
79.idea/caches/build_file_checksums.ser
80
81### PyCharm+all Patch ###
82# Ignores the whole .idea folder and all .iml files
83# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
84
85.idea/
86
87# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
88
89*.iml
90modules.xml
91.idea/misc.xml
92*.ipr
93
94# Sonarlint plugin
95.idea/sonarlint
96
97### Python ###
98# Byte-compiled / optimized / DLL files
99__pycache__/
100*.py[cod]
101*$py.class
102
103# C extensions
104*.so
105
106# Distribution / packaging
107.Python
108build/
109develop-eggs/
110dist/
111downloads/
112eggs/
113.eggs/
114lib/
115lib64/
116parts/
117sdist/
118var/
119wheels/
120share/python-wheels/
121*.egg-info/
122.installed.cfg
123*.egg
124MANIFEST
125
126# PyInstaller
127# Usually these files are written by a python script from a template
128# before PyInstaller builds the exe, so as to inject date/other infos into it.
129*.manifest
130*.spec
131
132# Installer logs
133pip-log.txt
134pip-delete-this-directory.txt
135
136# Unit test / coverage reports
137htmlcov/
138.tox/
139.nox/
140.coverage
141.coverage.*
142.cache
143nosetests.xml
144coverage.xml
145*.cover
146*.py,cover
147.hypothesis/
148.pytest_cache/
149cover/
150
151# Translations
152*.mo
153*.pot
154
155# Django stuff:
156*.log
157local_settings.py
158db.sqlite3
159db.sqlite3-journal
160
161# Flask stuff:
162instance/
163.webassets-cache
164
165# Scrapy stuff:
166.scrapy
167
168# Sphinx documentation
169docs/_build/
170
171# PyBuilder
172.pybuilder/
173target/
174
175# Jupyter Notebook
176.ipynb_checkpoints
177
178# IPython
179profile_default/
180ipython_config.py
181
182# pyenv
183# For a library or package, you might want to ignore these files since the code is
184# intended to run in multiple environments; otherwise, check them in:
185# .python-version
186
187# pipenv
188# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
189# However, in case of collaboration, if having platform-specific dependencies or dependencies
190# having no cross-platform support, pipenv may install dependencies that don't work, or not
191# install all needed dependencies.
192#Pipfile.lock
193
194# PEP 582; used by e.g. github.com/David-OConnor/pyflow
195__pypackages__/
196
197# Celery stuff
198celerybeat-schedule
199celerybeat.pid
200
201# SageMath parsed files
202*.sage.py
203
204# Environments
205.env
206.venv
207env/
208venv/
209ENV/
210env.bak/
211venv.bak/
212
213# Spyder project settings
214.spyderproject
215.spyproject
216
217# Rope project settings
218.ropeproject
219
220# mkdocs documentation
221/site
222
223# mypy
224.mypy_cache/
225.dmypy.json
226dmypy.json
227
228# Pyre type checker
229.pyre/
230
231# pytype static type analyzer
232.pytype/
233
234# Cython debug symbols
235cython_debug/
236
237# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..30291cb
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
3.10.0
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..66b4b70
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
1FROM python:3.10-alpine AS base
2
3RUN mkdir -p /app
4WORKDIR /app
5
6FROM base AS build
7
8RUN mkdir /buildroot
9RUN apk add gcc make musl-dev
10RUN pip install --upgrade pip
11
12COPY requirements.txt .
13RUN pip install -r requirements.txt --root /buildroot
14
15FROM base
16
17COPY --from=build /buildroot /
18COPY . .
19
20ENV TUNNEL_DOMAIN=
21ENV SECURE True
22EXPOSE 8000
23
24CMD ["uvicorn", "ttun_server:server", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a83ceb2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
1BSD 3-Clause License
2
3Copyright (c) 2022, Tom van der Lee
4All rights reserved.
5
6Redistribution and use in source and binary forms, with or without
7modification, are permitted provided that the following conditions are met:
8
91. Redistributions of source code must retain the above copyright notice, this
10 list of conditions and the following disclaimer.
11
122. Redistributions in binary form must reproduce the above copyright notice,
13 this list of conditions and the following disclaimer in the documentation
14 and/or other materials provided with the distribution.
15
163. Neither the name of the copyright holder nor the names of its
17 contributors may be used to endorse or promote products derived from
18 this software without specific prior written permission.
19
20THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..8688b23
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,41 @@
1===========
2TTUN Server
3===========
4
5|Release|
6
7.. |Release| image:: https://github.com/tomvanderlee/ttun-server/actions/workflows/docker-image.yml/badge.svg
8 :target: https://github.com/tomvanderlee/ttun-server/actions/workflows/docker-image.yml
9
10The self-hostable proxy tunnel.
11
12Running
13-------
14
15Running::
16
17 docker run -e TUNNEL_DOMAIN=<Your tunnel domain> -e SECURE=<True if using SSL> ghcr.io/tomvanderlee/ttun-server:latest
18
19
20Environment variables:
21
22+----------------+-----------------------------------------------------------------------------------------------------------------+--------------+
23| Variable | Description | Valid Value |
24+================+=================================================================================================================+==============+
25| TUNNEL_DOMAIN | The domain your tunnel server is hosted on. Any individual tunnels will be hosted as a subdomain of this one. | FQDN |
26+----------------+-----------------------------------------------------------------------------------------------------------------+--------------+
27| SECURE | Set this value to True if you are hosting the tunnel with SSL. If not leave this variable out | |
28+----------------+-----------------------------------------------------------------------------------------------------------------+--------------+
29
30Developing
31----------
32
331. Create and activate a python 3.10 virtual environment
34
352. Install requirements::
36
37 pip install -r requirements.txt
38
393. Run::
40
41 uvicorn ttun_server:server --reload
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..95f7ad2
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
1starlette ~= 0.17
2uvicorn[standard] ~= 0.16
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]