Repository: opsdroid/opsdroid

Issue:
Add possibility to configure CORS in routes
While testing #1930 with opsdroid-web I've realised that we aren't handling CORS at all. This means that any request that we will try to do from the app will fail since we can't include an  `Authorization`  header when using `no-cors` and if we use `cors` then opsdroid will reply with `OPTIONS`.

We should probably expand opsdroid web to allow users to configure cors on every route. I've been playing around with `aiohttp_cors` and seems to be pretty straightforward. For example:

```python
cors = aiohttp_cors.setup(self.web_app)
for route in self.web_app.router.routes():
    cors.add(route, {
         "*": aiohttp_cors.ResourceOptions(
         allow_headers=("Content-Type", "Authorization"),
         allow_methods="*",
         allow_credentials=True,
     )}
)
```

Perhaps we could allow users to override some of these settings  🤔 

Let me know what you think


Discussion:
Annoyingly, this doesn't seem to be as simple as that. The request goes through, but then something breaks. Will need to figure it out before deciding on this

```python
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_protocol.py", line 514, in start
    resp, reset = await task
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_protocol.py", line 460, in _handle_request
    reset = await self.finish_response(request, resp, start_time)
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_protocol.py", line 613, in finish_response
    await prepare_meth(request)
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_response.py", line 421, in prepare
    return await self._start(request)
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_response.py", line 764, in _start
    return await super()._start(request)
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_response.py", line 428, in _start
    await request._prepare_hook(self)
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp/web_request.py", line 874, in _prepare_hook
    await app.on_response_prepare.send(self, response)
  File "/opt/homebrew/lib/python3.9/site-packages/aiosignal/__init__.py", line 36, in send
    await receiver(*args, **kwargs)  # type: ignore
  File "/opt/homebrew/lib/python3.9/site-packages/aiohttp_cors/cors_config.py", line 171, in _on_response_prepare
    assert hdrs.ACCESS_CONTROL_ALLOW_ORIGIN not in response.headers
```

Resolving patch:
diff --git a/opsdroid/web.py b/opsdroid/web.py
--- a/opsdroid/web.py
+++ b/opsdroid/web.py
@@ -12,6 +12,7 @@
 from aiohttp import web
 from aiohttp.web import HTTPForbidden
 from aiohttp.web_exceptions import HTTPBadRequest
+from aiohttp_middlewares.cors import cors_middleware, DEFAULT_ALLOW_HEADERS
 
 from opsdroid import __version__
 from opsdroid.const import EXCLUDED_CONFIG_KEYS
@@ -19,6 +20,23 @@
 
 _LOGGER = logging.getLogger(__name__)
 
+DEFAULT_HEADERS = list(
+    DEFAULT_ALLOW_HEADERS
+    + (
+        "Host",
+        "Content-Type",
+        "Origin",
+        "Content-Length",
+        "Connection",
+        "Accept",
+        "User-Agent",
+        "Referer",
+        "Accept-Language",
+        "Accept-Encoding",
+        "Authorization",
+    )
+)
+
 
 @dataclasses.dataclass
 class Payload:
@@ -76,7 +94,16 @@ def __init__(self, opsdroid):
             self.config = self.opsdroid.config["web"]
         except KeyError:
             self.config = {}
-        self.web_app = web.Application()
+        self.cors = self.config.get("cors", {})
+        self.web_app = web.Application(
+            middlewares=[
+                cors_middleware(
+                    allow_all=self.cors.get("allow-all", True),
+                    origins=self.cors.get("origins", ["*"]),
+                    allow_headers=DEFAULT_HEADERS + self.cors.get("allow-headers", []),
+                )
+            ]
+        )
         self.runner = web.AppRunner(self.web_app)
         self.site = None
         self.command_center = self.config.get("command-center", {})


Test patch: