mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #3422 from pypeclub/feature/add_webserver_middleware
Webserver: Added CORS middleware
This commit is contained in:
commit
45d723a2e5
4 changed files with 312 additions and 2 deletions
283
openpype/modules/webserver/cors_middleware.py
Normal file
283
openpype/modules/webserver/cors_middleware.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
r"""
|
||||
===============
|
||||
CORS Middleware
|
||||
===============
|
||||
.. versionadded:: 0.2.0
|
||||
Dealing with CORS headers for aiohttp applications.
|
||||
**IMPORTANT:** There is a `aiohttp-cors
|
||||
<https://pypi.org/project/aiohttp_cors/>`_ library, which handles CORS
|
||||
headers by attaching additional handlers to aiohttp application for
|
||||
OPTIONS (preflight) requests. In same time this CORS middleware mimics the
|
||||
logic of `django-cors-headers <https://pypi.org/project/django-cors-headers>`_,
|
||||
where all handling done in the middleware without any additional handlers. This
|
||||
approach allows aiohttp application to respond with CORS headers for OPTIONS or
|
||||
wildcard handlers, which is not possible with ``aiohttp-cors`` due to
|
||||
https://github.com/aio-libs/aiohttp-cors/issues/241 issue.
|
||||
For detailed information about CORS (Cross Origin Resource Sharing) please
|
||||
visit:
|
||||
- `Wikipedia <https://en.m.wikipedia.org/wiki/Cross-origin_resource_sharing>`_
|
||||
- Or `MDN <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_
|
||||
Configuration
|
||||
=============
|
||||
**IMPORTANT:** By default, CORS middleware do not allow any origins to access
|
||||
content from your aiohttp appliction. Which means, you need carefully check
|
||||
possible options and provide custom values for your needs.
|
||||
Usage
|
||||
=====
|
||||
.. code-block:: python
|
||||
import re
|
||||
from aiohttp import web
|
||||
from aiohttp_middlewares import cors_middleware
|
||||
from aiohttp_middlewares.cors import DEFAULT_ALLOW_HEADERS
|
||||
# Unsecure configuration to allow all CORS requests
|
||||
app = web.Application(
|
||||
middlewares=[cors_middleware(allow_all=True)]
|
||||
)
|
||||
# Allow CORS requests from URL http://localhost:3000
|
||||
app = web.Application(
|
||||
middlewares=[
|
||||
cors_middleware(origins=["http://localhost:3000"])
|
||||
]
|
||||
)
|
||||
# Allow CORS requests from all localhost urls
|
||||
app = web.Application(
|
||||
middlewares=[
|
||||
cors_middleware(
|
||||
origins=[re.compile(r"^https?\:\/\/localhost")]
|
||||
)
|
||||
]
|
||||
)
|
||||
# Allow CORS requests from https://frontend.myapp.com as well
|
||||
# as allow credentials
|
||||
CORS_ALLOW_ORIGINS = ["https://frontend.myapp.com"]
|
||||
app = web.Application(
|
||||
middlewares=[
|
||||
cors_middleware(
|
||||
origins=CORS_ALLOW_ORIGINS,
|
||||
allow_credentials=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
# Allow CORS requests only for API urls
|
||||
app = web.Application(
|
||||
middelwares=[
|
||||
cors_middleware(
|
||||
origins=CORS_ALLOW_ORIGINS,
|
||||
urls=[re.compile(r"^\/api")],
|
||||
)
|
||||
]
|
||||
)
|
||||
# Allow CORS requests for POST & PATCH methods, and for all
|
||||
# default headers and `X-Client-UID`
|
||||
app = web.Application(
|
||||
middlewares=[
|
||||
cors_middleware(
|
||||
origings=CORS_ALLOW_ORIGINS,
|
||||
allow_methods=("POST", "PATCH"),
|
||||
allow_headers=DEFAULT_ALLOW_HEADERS
|
||||
+ ("X-Client-UID",),
|
||||
)
|
||||
]
|
||||
)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Pattern, Tuple
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from aiohttp_middlewares.annotations import (
|
||||
Handler,
|
||||
Middleware,
|
||||
StrCollection,
|
||||
UrlCollection,
|
||||
)
|
||||
from aiohttp_middlewares.utils import match_path
|
||||
|
||||
|
||||
ACCESS_CONTROL = "Access-Control"
|
||||
ACCESS_CONTROL_ALLOW = f"{ACCESS_CONTROL}-Allow"
|
||||
ACCESS_CONTROL_ALLOW_CREDENTIALS = f"{ACCESS_CONTROL_ALLOW}-Credentials"
|
||||
ACCESS_CONTROL_ALLOW_HEADERS = f"{ACCESS_CONTROL_ALLOW}-Headers"
|
||||
ACCESS_CONTROL_ALLOW_METHODS = f"{ACCESS_CONTROL_ALLOW}-Methods"
|
||||
ACCESS_CONTROL_ALLOW_ORIGIN = f"{ACCESS_CONTROL_ALLOW}-Origin"
|
||||
ACCESS_CONTROL_EXPOSE_HEADERS = f"{ACCESS_CONTROL}-Expose-Headers"
|
||||
ACCESS_CONTROL_MAX_AGE = f"{ACCESS_CONTROL}-Max-Age"
|
||||
ACCESS_CONTROL_REQUEST_METHOD = f"{ACCESS_CONTROL}-Request-Method"
|
||||
|
||||
DEFAULT_ALLOW_HEADERS = (
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"authorization",
|
||||
"content-type",
|
||||
"dnt",
|
||||
"origin",
|
||||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
)
|
||||
DEFAULT_ALLOW_METHODS = ("DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT")
|
||||
DEFAULT_URLS: Tuple[Pattern[str]] = (re.compile(r".*"),)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def cors_middleware(
|
||||
*,
|
||||
allow_all: bool = False,
|
||||
origins: UrlCollection = None,
|
||||
urls: UrlCollection = None,
|
||||
expose_headers: StrCollection = None,
|
||||
allow_headers: StrCollection = DEFAULT_ALLOW_HEADERS,
|
||||
allow_methods: StrCollection = DEFAULT_ALLOW_METHODS,
|
||||
allow_credentials: bool = False,
|
||||
max_age: int = None,
|
||||
) -> Middleware:
|
||||
"""Middleware to provide CORS headers for aiohttp applications.
|
||||
:param allow_all:
|
||||
When enabled, allow any Origin to access content from your aiohttp web
|
||||
application. **Please be careful with enabling this option as it may
|
||||
result in security issues for your application.** By default: ``False``
|
||||
:param origins:
|
||||
Allow content access for given list of origins. Support supplying
|
||||
strings for exact origin match or regex instances. By default: ``None``
|
||||
:param urls:
|
||||
Allow contect access for given list of URLs in aiohttp application.
|
||||
By default: *apply CORS headers for all URLs*
|
||||
:param expose_headers:
|
||||
List of headers to be exposed with every CORS request. By default:
|
||||
``None``
|
||||
:param allow_headers:
|
||||
List of allowed headers. By default:
|
||||
.. code-block:: python
|
||||
(
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"authorization",
|
||||
"content-type",
|
||||
"dnt",
|
||||
"origin",
|
||||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
)
|
||||
:param allow_methods:
|
||||
List of allowed methods. By default:
|
||||
.. code-block:: python
|
||||
("DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT")
|
||||
:param allow_credentials:
|
||||
When enabled apply allow credentials header in response, which results
|
||||
in sharing cookies on shared resources. **Please be careful with
|
||||
allowing credentials for CORS requests.** By default: ``False``
|
||||
:param max_age: Access control max age in seconds. By default: ``None``
|
||||
"""
|
||||
check_urls: UrlCollection = DEFAULT_URLS if urls is None else urls
|
||||
|
||||
@web.middleware
|
||||
async def middleware(
|
||||
request: web.Request, handler: Handler
|
||||
) -> web.StreamResponse:
|
||||
# Initial vars
|
||||
request_method = request.method
|
||||
request_path = request.rel_url.path
|
||||
|
||||
# Is this an OPTIONS request
|
||||
is_options_request = request_method == "OPTIONS"
|
||||
|
||||
# Is this a preflight request
|
||||
is_preflight_request = (
|
||||
is_options_request
|
||||
and ACCESS_CONTROL_REQUEST_METHOD in request.headers
|
||||
)
|
||||
|
||||
# Log extra data
|
||||
log_extra = {
|
||||
"is_preflight_request": is_preflight_request,
|
||||
"method": request_method.lower(),
|
||||
"path": request_path,
|
||||
}
|
||||
|
||||
# Check whether CORS should be enabled for given URL or not. By default
|
||||
# CORS enabled for all URLs
|
||||
if not match_items(check_urls, request_path):
|
||||
logger.debug(
|
||||
"Request should not be processed via CORS middleware",
|
||||
extra=log_extra,
|
||||
)
|
||||
return await handler(request)
|
||||
|
||||
# If this is a preflight request - generate empty response
|
||||
if is_preflight_request:
|
||||
response = web.StreamResponse()
|
||||
# Otherwise - call actual handler
|
||||
else:
|
||||
response = await handler(request)
|
||||
|
||||
# Now check origin heaer
|
||||
origin = request.headers.get("Origin")
|
||||
# Empty origin - do nothing
|
||||
if not origin:
|
||||
logger.debug(
|
||||
"Request does not have Origin header. CORS headers not "
|
||||
"available for given requests",
|
||||
extra=log_extra,
|
||||
)
|
||||
return response
|
||||
|
||||
# Set allow credentials header if necessary
|
||||
if allow_credentials:
|
||||
response.headers[ACCESS_CONTROL_ALLOW_CREDENTIALS] = "true"
|
||||
|
||||
# Check whether current origin satisfies CORS policy
|
||||
if not allow_all and not (origins and match_items(origins, origin)):
|
||||
logger.debug(
|
||||
"CORS headers not allowed for given Origin", extra=log_extra
|
||||
)
|
||||
return response
|
||||
|
||||
# Now start supplying CORS headers
|
||||
# First one is Access-Control-Allow-Origin
|
||||
if allow_all and not allow_credentials:
|
||||
cors_origin = "*"
|
||||
else:
|
||||
cors_origin = origin
|
||||
response.headers[ACCESS_CONTROL_ALLOW_ORIGIN] = cors_origin
|
||||
|
||||
# Then Access-Control-Expose-Headers
|
||||
if expose_headers:
|
||||
response.headers[ACCESS_CONTROL_EXPOSE_HEADERS] = ", ".join(
|
||||
expose_headers
|
||||
)
|
||||
|
||||
# Now, if this is an options request, respond with extra Allow headers
|
||||
if is_options_request:
|
||||
response.headers[ACCESS_CONTROL_ALLOW_HEADERS] = ", ".join(
|
||||
allow_headers
|
||||
)
|
||||
response.headers[ACCESS_CONTROL_ALLOW_METHODS] = ", ".join(
|
||||
allow_methods
|
||||
)
|
||||
if max_age is not None:
|
||||
response.headers[ACCESS_CONTROL_MAX_AGE] = str(max_age)
|
||||
|
||||
# If this is preflight request - do not allow other middlewares to
|
||||
# process this request
|
||||
if is_preflight_request:
|
||||
logger.debug(
|
||||
"Provide CORS headers with empty response for preflight "
|
||||
"request",
|
||||
extra=log_extra,
|
||||
)
|
||||
raise web.HTTPOk(text="", headers=response.headers)
|
||||
|
||||
# Otherwise return normal response
|
||||
logger.debug("Provide CORS headers for request", extra=log_extra)
|
||||
return response
|
||||
|
||||
return middleware
|
||||
|
||||
|
||||
def match_items(items: UrlCollection, value: str) -> bool:
|
||||
"""Go through all items and try to match item with given value."""
|
||||
return any(match_path(item, value) for item in items)
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
import re
|
||||
import threading
|
||||
import asyncio
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
from .cors_middleware import cors_middleware
|
||||
|
||||
log = PypeLogger.get_logger("WebServer")
|
||||
|
||||
|
||||
class WebServerManager:
|
||||
"""Manger that care about web server thread."""
|
||||
|
||||
def __init__(self, port=None, host=None):
|
||||
self.port = port or 8079
|
||||
self.host = host or "localhost"
|
||||
|
|
@ -18,7 +21,13 @@ class WebServerManager:
|
|||
self.handlers = {}
|
||||
self.on_stop_callbacks = []
|
||||
|
||||
self.app = web.Application()
|
||||
self.app = web.Application(
|
||||
middlewares=[
|
||||
cors_middleware(
|
||||
origins=[re.compile(r"^https?\:\/\/localhost")]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# add route with multiple methods for single "external app"
|
||||
|
||||
|
|
|
|||
17
poetry.lock
generated
17
poetry.lock
generated
|
|
@ -46,6 +46,19 @@ python-versions = ">=3.5"
|
|||
[package.dependencies]
|
||||
aiohttp = ">=3,<4"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp-middlewares"
|
||||
version = "2.0.0"
|
||||
description = "Collection of useful middlewares for aiohttp applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.8.1,<4.0.0"
|
||||
async-timeout = ">=4.0.2,<5.0.0"
|
||||
yarl = ">=1.5.1,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.2.0"
|
||||
|
|
@ -1783,6 +1796,10 @@ aiohttp-json-rpc = [
|
|||
{file = "aiohttp-json-rpc-0.13.3.tar.gz", hash = "sha256:6237a104478c22c6ef96c7227a01d6832597b414e4b79a52d85593356a169e99"},
|
||||
{file = "aiohttp_json_rpc-0.13.3-py3-none-any.whl", hash = "sha256:4fbd197aced61bd2df7ae3237ead7d3e08833c2ccf48b8581e1828c95ebee680"},
|
||||
]
|
||||
aiohttp-middlewares = [
|
||||
{file = "aiohttp-middlewares-2.0.0.tar.gz", hash = "sha256:e08ba04dc0e8fe379aa5e9444a68485c275677ee1e18c55cbb855de0c3629502"},
|
||||
{file = "aiohttp_middlewares-2.0.0-py3-none-any.whl", hash = "sha256:29cf1513176b4013844711975ff520e26a8a5d8f9fefbbddb5e91224a86b043e"},
|
||||
]
|
||||
aiosignal = [
|
||||
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
||||
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ slack-sdk = "^3.6.0"
|
|||
requests = "^2.25.1"
|
||||
pysftp = "^0.2.9"
|
||||
dropbox = "^11.20.0"
|
||||
aiohttp-middlewares = "^2.0.0"
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
|
@ -154,4 +155,4 @@ exclude = [
|
|||
ignore = ["website", "docs", ".git"]
|
||||
|
||||
reportMissingImports = true
|
||||
reportMissingTypeStubs = false
|
||||
reportMissingTypeStubs = false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue