diff --git a/poetry.lock b/poetry.lock index bd1f119..68eefdb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -560,6 +560,14 @@ dev = ["Cython (>=0.29.20,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0 docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"] test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +[[package]] +name = "websockets" +version = "9.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.6.1" + [[package]] name = "zipp" version = "3.4.1" @@ -588,7 +596,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "8b1f2a003502e099b7a3995cfbbbc413401adf08c391d3b07e749e919916ab85" +content-hash = "8e1e4f30b4d5f3cba3dc4b594a872f149b9aa3eeb6d3b5b721f5847c6aa2fd84" [metadata.files] asgiref = [ @@ -866,6 +874,41 @@ uvloop = [ {file = "uvloop-0.15.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c"}, {file = "uvloop-0.15.2.tar.gz", hash = "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01"}, ] +websockets = [ + {file = "websockets-9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da"}, + {file = "websockets-9.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4"}, + {file = "websockets-9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314"}, + {file = "websockets-9.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58"}, + {file = "websockets-9.1-cp36-cp36m-win32.whl", hash = "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a"}, + {file = "websockets-9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b"}, + {file = "websockets-9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af"}, + {file = "websockets-9.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"}, + {file = "websockets-9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25"}, + {file = "websockets-9.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2"}, + {file = "websockets-9.1-cp37-cp37m-win32.whl", hash = "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a"}, + {file = "websockets-9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857"}, + {file = "websockets-9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe"}, + {file = "websockets-9.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0"}, + {file = "websockets-9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02"}, + {file = "websockets-9.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f"}, + {file = "websockets-9.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec"}, + {file = "websockets-9.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc"}, + {file = "websockets-9.1-cp38-cp38-win32.whl", hash = "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e"}, + {file = "websockets-9.1-cp38-cp38-win_amd64.whl", hash = "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077"}, + {file = "websockets-9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2"}, + {file = "websockets-9.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d"}, + {file = "websockets-9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d"}, + {file = "websockets-9.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c"}, + {file = "websockets-9.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135"}, + {file = "websockets-9.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd"}, + {file = "websockets-9.1-cp39-cp39-win32.whl", hash = "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20"}, + {file = "websockets-9.1-cp39-cp39-win_amd64.whl", hash = "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0"}, + {file = "websockets-9.1.tar.gz", hash = "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3"}, +] zipp = [ {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, diff --git a/pyproject.toml b/pyproject.toml index 06c847e..c10021d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ uvloop = "^0.15.2" djangorestframework-dataclasses = "^0.9" social-auth-app-django = "^4.0.0" python-dotenv = "^0.17.1" +websockets = "^9.1" [tool.poetry.dev-dependencies] flake8 = "^3.9.2" diff --git a/src/services/appConnector.service.ts b/src/services/appConnector.service.ts index f2d3627..a82d418 100644 --- a/src/services/appConnector.service.ts +++ b/src/services/appConnector.service.ts @@ -11,14 +11,13 @@ export class SocketProxy { webSocket: WebSocket initialBuffer: Buffer - constructor (...args) { - console.log('ws ctr', args) + constructor () { this.initialBuffer = Buffer.from('') } - connect (...args) { - console.log('ws connect', args) - this.webSocket = new WebSocket(`ws://${location.host}/api/1/gateway/tcp`) + connect (options) { + console.log('ws connect', options) + this.webSocket = new WebSocket(`ws://${location.host}/api/1/gateway/tcp/${options.host}:${options.port}`) this.webSocket.onopen = event => { this.connect$.next() this.connect$.complete() diff --git a/terminus/app/consumers.py b/terminus/app/consumers.py index 78ef87f..e9eee4b 100644 --- a/terminus/app/consumers.py +++ b/terminus/app/consumers.py @@ -1,31 +1,74 @@ import asyncio +import os +import ssl +import websockets from channels.generic.websocket import AsyncWebsocketConsumer +from django.conf import settings +from urllib.parse import quote + + +class GatewayConnection: + _ssl_context: ssl.SSLContext = None + + def __init__(self, host: str, port: int): + if settings.CONNECTION_GATEWAY_AUTH_KEY and not GatewayConnection._ssl_context: + ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + ctx.load_cert_chain( + os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CERTIFICATE), + os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_KEY), + ) + if settings.CONNECTION_GATEWAY_AUTH_CA: + ctx.load_verify_locations( + cafile=os.path.realpath(settings.CONNECTION_GATEWAY_AUTH_CA), + ) + ctx.verify_mode = ssl.CERT_REQUIRED + GatewayConnection._ssl_context = ctx + + proto = 'wss' if GatewayConnection._ssl_context else 'ws' + self.url = f'{proto}://localhost:9000/connect/{quote(host)}:{quote(str(port))}' + + async def connect(self): + self.context = websockets.connect(self.url, ssl=GatewayConnection._ssl_context) + self.socket = await self.context.__aenter__() + + async def send(self, data): + await self.socket.send(data) + + def recv(self, timeout=None): + return asyncio.wait_for(self.socket.recv(), timeout) + + async def close(self): + await self.socket.close() + await self.context.__aexit__(None, None, None) class TCPConsumer(AsyncWebsocketConsumer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.loop = asyncio.get_event_loop() - async def connect(self): - self.reader, self.writer = await asyncio.open_connection('192.168.78.233', 22) - self._socket_reader = self.loop.create_task(self.socket_reader()) + self.closed = False + self.conn = GatewayConnection( + self.scope['url_route']['kwargs']['host'], + int(self.scope['url_route']['kwargs']['port']), + ) + await self.conn.connect() await self.accept() + self.reader = asyncio.get_event_loop().create_task(self.socket_reader()) async def disconnect(self, close_code): - await self.writer.drain() - self.writer.close() - await self._socket_reader + self.closed = True + await self.conn.close() async def receive(self, bytes_data): - self.writer.write(bytes_data) + await self.conn.send(bytes_data) async def socket_reader(self): while True: - await self.reader._wait_for_data('read') - data = bytes(self.reader._buffer.copy()) - if not data: + if self.closed: + return + try: + data = await self.conn.recv(timeout=10) + except asyncio.TimeoutError: + continue + except websockets.exceptions.ConnectionClosed: await self.close() return - del self.reader._buffer[:] await self.send(bytes_data=data) diff --git a/terminus/app/urls.py b/terminus/app/urls.py index 4b12470..40a6e3e 100644 --- a/terminus/app/urls.py +++ b/terminus/app/urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +from django.urls import path, re_path, include from rest_framework import routers from . import api @@ -22,5 +22,5 @@ urlpatterns = [ ] websocket_urlpatterns = [ - path('api/1/gateway/tcp', consumers.TCPConsumer.as_asgi()), + re_path(r'^api/1/gateway/tcp/(?P<host>[^/]+):(?P<port>\d+)$', consumers.TCPConsumer.as_asgi()), ] diff --git a/terminus/settings.py b/terminus/settings.py index e62e8ff..101a310 100644 --- a/terminus/settings.py +++ b/terminus/settings.py @@ -149,5 +149,17 @@ for key in [ 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET', 'SOCIAL_AUTH_MICROSOFT_GRAPH_KEY', 'SOCIAL_AUTH_MICROSOFT_GRAPH_SECRET', + 'CONNECTION_GATEWAY_AUTH_CA', + 'CONNECTION_GATEWAY_AUTH_CERTIFICATE', + 'CONNECTION_GATEWAY_AUTH_KEY', ]: globals()[key] = os.getenv(key) + +for key in [ + 'CONNECTION_GATEWAY_AUTH_CA', + 'CONNECTION_GATEWAY_AUTH_CERTIFICATE', + 'CONNECTION_GATEWAY_AUTH_KEY', +]: + v = globals()[key] + if v and not os.path.exists(v): + raise ValueError(f'{v} does not exist')