Add proxy functionality.
authorBenjamin Braatz <bb@bbraatz.eu>
Tue, 5 Oct 2021 22:02:36 +0000 (00:02 +0200)
committerBenjamin Braatz <bb@bbraatz.eu>
Tue, 5 Oct 2021 22:02:36 +0000 (00:02 +0200)
conf.json
controlpi_plugins/wsserver.py
setup.py
web/index.html

index adf56e12c556f94c948e08c0cdb3661650e0a679..f814b0d85b639df576d0bb0f87888385573ea6c5 100644 (file)
--- 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": {
index 4c369c8c2e16a45b0b62e0fa734b09eeb91e0c0d..599a856cd0ec77b0efbebf8257367ae8760eb8fe 100644 (file)
@@ -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."""
index dce9af5d7b4cf2ce32fdbf1ee568a686023a886d..a7faa0841d3bf7c98dfcd8c157e84b6237990772 100644 (file)
--- 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",
     ],
index 59c384e26c8396d898797d84229455d624220dc5..998a5857f2164a354d7e53cebde640ccbd67e63b 100644 (file)
@@ -24,5 +24,6 @@
         </header>
         <h1>ControlPi</h1>
         <h2><a href="Debug/">Debug</a></h2>
+        <h2><a href="Proxy/">Proxy</a></h2>
     </body>
 </html>