From: Benjamin Braatz Date: Tue, 5 Oct 2021 22:02:36 +0000 (+0200) Subject: Add proxy functionality. X-Git-Tag: v0.3.0~12 X-Git-Url: http://git.graph-it.com/?a=commitdiff_plain;h=f22c2f2aca20f008f4859fbdcaa1fd2b4e06a08a;p=graphit%2Fcontrolpi-wsserver.git Add proxy functionality. --- diff --git a/conf.json b/conf.json index adf56e1..f814b0d 100644 --- a/conf.json +++ b/conf.json @@ -5,7 +5,8 @@ "web": { "/": {"location": "web"}, "/Debug": {"module": "controlpi_plugins.wsserver", - "location": "Debug"} + "location": "Debug"}, + "/Proxy": {"url": "http://localhost:8080/Debug"} } }, "Example State": { diff --git a/controlpi_plugins/wsserver.py b/controlpi_plugins/wsserver.py index 4c369c8..599a856 100644 --- a/controlpi_plugins/wsserver.py +++ b/controlpi_plugins/wsserver.py @@ -1,4 +1,5 @@ import aiofiles +import aiohttp import asyncio import http import json @@ -92,10 +93,9 @@ class Connection: else: await self._bus.send(Message(self._name, message)) except ConnectionClosed: - pass - await self._bus.send(Message(self._name, - {'event': 'connection closed'})) - self._bus.unregister(self._name) + await self._bus.send(Message(self._name, + {'event': 'connection closed'})) + self._bus.unregister(self._name) Response = Optional[Tuple[http.HTTPStatus, Headers, bytes]] @@ -113,11 +113,15 @@ class WSServer(BasePlugin): 'port': {'type': 'integer'}, 'web': {'type': 'object', 'patternProperties': - {'^/([A-Z][A-Za-z]*)?$': - {'type': 'object', - 'properties': {'module': {'type': 'string'}, - 'location': {'type': 'string'}}, - 'required': ['location']}}, + {'^/[A-Za-z0-9]*$': + {'anyOf': + [{'type': 'object', + 'properties': {'module': {'type': 'string'}, + 'location': {'type': 'string'}}, + 'required': ['location']}, + {'type': 'object', + 'properties': {'url': {'type': 'string'}}, + 'required': ['url']}]}}, 'additionalProperties': False}}} """Schema for WSServer plugin configuration. @@ -128,13 +132,44 @@ class WSServer(BasePlugin): - 'port': port to connect to (default: 80) - 'web': mapping of web paths to locations on disk (either as absolute path, relative path from the working directory or path from the - path containing a given module) + path containing a given module) or proxies to URLs """ - async def _handler(self, websocket: WebSocketServerProtocol, - path: str) -> None: - connection = Connection(self.bus, websocket) - await connection.run() + def process_conf(self) -> None: + """Get host, port and path settings from configuration.""" + self._host = None + if 'host' in self.conf: + self._host = self.conf['host'] + else: + print(f"'host' not configured for WSServer '{self.name}'." + " Serving on all interfaces.") + self._port = 80 + if 'port' in self.conf: + self._port = self.conf['port'] + else: + print(f"'port' not configured for WSServer '{self.name}'." + " Using port 80.") + self._web_locations = {} + self._web_proxies = {} + if 'web' in self.conf: + for path in self.conf['web']: + path_conf = self.conf['web'][path] + if 'location' in path_conf: + location = path_conf['location'] + if 'module' in path_conf: + # Determine location relative to module directory: + module_file = sys.modules[path_conf['module']].__file__ + module_dir = os.path.dirname(module_file) + location = os.path.join(module_dir, 'web', location) + else: + # Determine location relative to working directory: + location = os.path.join(os.getcwd(), location) + self._web_locations[path] = os.path.realpath(location) + elif 'url' in path_conf: + base_url = path_conf['url'] + if not base_url.endswith('/'): + base_url += '/' + self._web_proxies[path] = base_url async def _process_request(self, path: str, request_headers: Headers) -> Response: @@ -146,16 +181,26 @@ class WSServer(BasePlugin): response_headers['Server'] = 'controlpi-wsserver websocket server' response_headers['Connection'] = 'close' location = '' + url = '' start_path_length = 0 for start_path in self._web_locations: if (path.startswith(start_path) and len(start_path) > start_path_length): start_path_length = len(start_path) - location_path = self._web_locations[start_path] + start_location = self._web_locations[start_path] + if not start_path.endswith('/'): + start_path += '/' + relative_path = path[len(start_path):] + location = os.path.join(start_location, relative_path) + for start_path in self._web_proxies: + if (path.startswith(start_path) and + len(start_path) > start_path_length): + start_path_length = len(start_path) + base_url = self._web_proxies[start_path] if not start_path.endswith('/'): start_path += '/' relative_path = path[len(start_path):] - location = os.path.join(location_path, relative_path) + url = base_url + relative_path if location: if os.path.isdir(location) and not path.endswith('/'): status = http.HTTPStatus.MOVED_PERMANENTLY @@ -181,6 +226,15 @@ class WSServer(BasePlugin): async with aiofiles.open(location, 'rb') as f: body = await f.read() response_headers['Content-Length'] = str(len(body)) + if url: + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + status = http.HTTPStatus.OK + response_headers['Content-Type'] = \ + resp.headers['Content-Type'] + response_headers['Content-Length'] = \ + resp.headers['Content-Length'] + body = await resp.read() if not status: status = http.HTTPStatus.NOT_FOUND body = f"'{path}' not found!".encode() @@ -188,34 +242,10 @@ class WSServer(BasePlugin): response_headers['Content-Length'] = str(len(body)) return status, response_headers, body - def process_conf(self) -> None: - """Get host, port and path settings from configuration.""" - self._host = None - if 'host' in self.conf: - self._host = self.conf['host'] - else: - print(f"'host' not configured for WSServer '{self.name}'." - " Serving on all interfaces.") - self._port = 80 - if 'port' in self.conf: - self._port = self.conf['port'] - else: - print(f"'port' not configured for WSServer '{self.name}'." - " Using port 80.") - self._web_locations = {} - if 'web' in self.conf: - for path in self.conf['web']: - path_conf = self.conf['web'][path] - location = path_conf['location'] - if 'module' in path_conf: - # Determine location relative to module directory: - module_file = sys.modules[path_conf['module']].__file__ - module_dir = os.path.dirname(module_file) - location = os.path.join(module_dir, 'web', location) - else: - # Determine location relative to current working directory: - location = os.path.join(os.getcwd(), location) - self._web_locations[path] = os.path.realpath(location) + async def _handler(self, websocket: WebSocketServerProtocol, + path: str) -> None: + connection = Connection(self.bus, websocket) + await connection.run() async def run(self) -> None: """Set up websocket server.""" diff --git a/setup.py b/setup.py index dce9af5..a7faa08 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ setuptools.setup( include_package_data=True, install_requires=[ "aiofiles", + "aiohttp", "websockets", "controlpi @ git+git://git.graph-it.com/graphit/controlpi.git@master", ], diff --git a/web/index.html b/web/index.html index 59c384e..998a585 100644 --- a/web/index.html +++ b/web/index.html @@ -24,5 +24,6 @@

ControlPi

Debug

+

Proxy