Initial commit: IT Site Survey AI v2 with all features

This commit is contained in:
BeawIT Admin
2026-07-03 02:06:55 +00:00
commit b4fff79eda
1985 changed files with 437461 additions and 0 deletions
@@ -0,0 +1,23 @@
from importlib.metadata import PackageNotFoundError, version
from .decorator import cross_origin
from .extension import CORS
try:
__version__ = version("flask-cors")
except PackageNotFoundError: # pragma: no cover - package is not installed
__version__ = "unknown"
__all__ = ["CORS", "__version__", "cross_origin"]
# Set default logging handler to avoid "No handler found" warnings.
import logging
from logging import NullHandler
# Set initial level to WARN. Users must manually enable logging for
# flask_cors to see our logging.
rootlogger = logging.getLogger(__name__)
rootlogger.addHandler(NullHandler())
if rootlogger.level == logging.NOTSET:
rootlogger.setLevel(logging.WARN)
@@ -0,0 +1,565 @@
from __future__ import annotations
import logging
import re
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from datetime import timedelta
from typing import Any, TypedDict, Union, cast
from flask import Blueprint, Flask, Response, current_app, request
from werkzeug.datastructures import Headers, MultiDict
LOG = logging.getLogger(__name__)
# A resource/origin/header pattern may either be a literal string or a
# pre-compiled regular expression.
ResourcePattern = Union[str, "re.Pattern[str]"]
# What a user may pass as ``resources``: a single pattern, a list of patterns,
# or a mapping of pattern -> per-resource (raw) options.
ResourceSpec = Union[
str,
"re.Pattern[str]",
"list[ResourcePattern]",
"dict[ResourcePattern, dict[str, Any]]",
]
# Input shapes accepted from users for individual options, before they are
# normalized by :func:`serialize_options`. These are deliberately broad: a
# scalar or an iterable of scalars are both accepted in most cases.
OriginsInput = Union[ResourcePattern, "Iterable[ResourcePattern]"]
HeadersInput = Union[ResourcePattern, "Iterable[ResourcePattern]"]
MethodsInput = Union[str, "Iterable[str]"]
ExposeHeadersInput = Union[str, "Iterable[str]", None]
MaxAgeInput = Union[timedelta, int, str, None]
@dataclass(frozen=True)
class _ComputedCorsOptions:
"""The fully-resolved CORS options for a single route.
Private internal representation, built only by :func:`serialize_options`.
Configure CORS via :class:`~flask_cors.CORS` / :func:`cross_origin` keyword
arguments, not by constructing this directly.
"""
# ``origins``/``allow_headers`` are normalized to a list of patterns: each
# entry is either a compiled ``re.Pattern`` (regex) or a literal ``str``,
# so request-time matching never has to guess which it is.
origins: list[ResourcePattern]
allow_headers: list[ResourcePattern]
# Whether ``origins`` includes the catch-all ``.*`` wildcard. Computed once
# here because the wildcard is otherwise indistinguishable from a regex
# after the origins have been compiled.
allow_all_origins: bool
# ``methods``/``expose_headers`` join to a comma-separated string, or are
# ``None`` when not configured.
methods: str | None
expose_headers: str | None
# ``max_age`` is left as-is unless a timedelta is given, then stringified.
max_age: str | int | None
supports_credentials: bool
send_wildcard: bool
automatic_options: bool
vary_header: bool
intercept_exceptions: bool
always_send: bool
allow_private_network: bool
class CrossOriginOptionsInput(TypedDict, total=False):
"""The keyword options accepted by :func:`cross_origin`.
Every key is optional (``total=False``): only the options a caller
actually passes are forwarded, so app-level ``CORS_*`` configuration and
the defaults still apply to anything left out. Used with PEP 692
``Unpack`` to type ``**kwargs`` precisely.
"""
origins: OriginsInput
methods: MethodsInput
expose_headers: ExposeHeadersInput
allow_headers: HeadersInput
supports_credentials: bool
max_age: MaxAgeInput
send_wildcard: bool
vary_header: bool
automatic_options: bool
always_send: bool
allow_private_network: bool
class CorsOptionsInput(CrossOriginOptionsInput, total=False):
"""The keyword options accepted by :class:`CORS` / :meth:`CORS.init_app`.
Extends :class:`CrossOriginOptionsInput` with the application-level options that
only make sense for the extension.
"""
resources: ResourceSpec
intercept_exceptions: bool
# Response Headers
ACL_ORIGIN = "Access-Control-Allow-Origin"
ACL_METHODS = "Access-Control-Allow-Methods"
ACL_ALLOW_HEADERS = "Access-Control-Allow-Headers"
ACL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"
ACL_CREDENTIALS = "Access-Control-Allow-Credentials"
ACL_MAX_AGE = "Access-Control-Max-Age"
ACL_RESPONSE_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network"
# Request Header
ACL_REQUEST_METHOD = "Access-Control-Request-Method"
ACL_REQUEST_HEADERS = "Access-Control-Request-Headers"
ACL_REQUEST_HEADER_PRIVATE_NETWORK = "Access-Control-Request-Private-Network"
ALL_METHODS = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"]
CONFIG_OPTIONS = [
"CORS_ORIGINS",
"CORS_METHODS",
"CORS_ALLOW_HEADERS",
"CORS_EXPOSE_HEADERS",
"CORS_SUPPORTS_CREDENTIALS",
"CORS_MAX_AGE",
"CORS_SEND_WILDCARD",
"CORS_AUTOMATIC_OPTIONS",
"CORS_VARY_HEADER",
"CORS_RESOURCES",
"CORS_INTERCEPT_EXCEPTIONS",
"CORS_ALWAYS_SEND",
"CORS_ALLOW_PRIVATE_NETWORK",
]
# Attribute added to request object by decorator to indicate that CORS
# was evaluated, in case the decorator and extension are both applied
# to a view.
FLASK_CORS_EVALUATED = "_FLASK_CORS_EVALUATED"
# The type of a compiled regular expression. Exposed publicly as
# ``re.Pattern`` since Python 3.8.
RegexObject = re.Pattern
# Characters commonly found in regular expressions. Their presence is used as
# a heuristic for whether a string is a regex rather than a literal value.
_REGEX_HINT_CHARS = frozenset("*\\]?$^[]()")
# The set of recognized option names, derived from the input schema (which
# lists exactly the keys a user may pass; the resolved dataclass additionally
# carries computed fields such as ``allow_all_origins``).
_KNOWN_OPTIONS = frozenset(CorsOptionsInput.__required_keys__) | frozenset(CorsOptionsInput.__optional_keys__)
# The raw, un-normalized option defaults. These are merged with app config and
# user kwargs before being handed to :func:`serialize_options`, which produces
# the strongly-typed :class:`_ComputedCorsOptions`. Typing this as ``CorsOptionsInput``
# lets mypy verify the defaults match the documented input schema.
DEFAULT_OPTIONS: CorsOptionsInput = {
"origins": "*",
"methods": ALL_METHODS,
"allow_headers": "*",
"expose_headers": None,
"supports_credentials": False,
"max_age": None,
"send_wildcard": False,
"automatic_options": True,
"vary_header": True,
"resources": r"/*",
"intercept_exceptions": True,
"always_send": True,
"allow_private_network": False,
}
def parse_resources(resources: ResourceSpec) -> list[tuple[ResourcePattern, dict[str, Any]]]:
if isinstance(resources, dict):
# To make the API more consistent with the decorator, allow a
# resource of '*', which is not actually a valid regexp.
resource_pairs = [(re_fix(k), v) for k, v in resources.items()]
# Sort patterns with static (literal) paths first, then by regex specificity
def sort_key(pair: tuple[ResourcePattern, Any]) -> tuple[int, int, int, int]:
pattern, _ = pair
if isinstance(pattern, re.Pattern):
return (1, 0, -pattern.pattern.count("/"), -len(pattern.pattern))
elif probably_regex(pattern):
return (1, 1, -pattern.count("/"), -len(pattern))
else:
return (0, 0, -pattern.count("/"), -len(pattern))
resource_pairs.sort(key=sort_key)
elif isinstance(resources, str):
resource_pairs = [(re_fix(resources), {})]
elif isinstance(resources, Iterable):
resource_pairs = [(re_fix(r), {}) for r in resources]
# Type of compiled regex is not part of the public API. Test for this
# at runtime.
elif isinstance(resources, re.Pattern):
resource_pairs = [(re_fix(resources), {})]
else:
# ValueError (rather than the arguably more correct TypeError) is the
# historical public contract here; callers may catch it.
raise ValueError("Unexpected value for resources argument.") # noqa: TRY004
# Resolve each path pattern once (paths are matched case-sensitively), so
# request handling never re-runs the regex heuristic. Patterns are sorted
# above, before compilation, so specificity ordering is preserved.
return [(_resolve_pattern(pattern, ignore_case=False), opts) for (pattern, opts) in resource_pairs]
def get_regexp_pattern(regexp: ResourcePattern) -> str:
"""
Helper that returns regexp pattern from given value.
:param regexp: regular expression to stringify
:type regexp: re.Pattern or str
:returns: string representation of given regexp pattern
:rtype: str
"""
if isinstance(regexp, re.Pattern):
return regexp.pattern
return str(regexp)
def get_cors_origins(options: _ComputedCorsOptions, request_origin: str | None) -> list[str] | None:
origins = options.origins
wildcard = options.allow_all_origins
# If the Origin header is not present terminate this set of steps.
# The request is outside the scope of this specification.-- W3Spec
if request_origin:
LOG.debug("CORS request received with 'Origin' %s", request_origin)
# If the allowed origins is an asterisk or 'wildcard', always match
if wildcard and options.send_wildcard:
LOG.debug("Allowed origins are set to '*'. Sending wildcard CORS header.")
return ["*"]
# If the value of the Origin header is a case-insensitive match
# for any of the values in list of origins.
# NOTE: Per RFC 1035 and RFC 4343 schemes and hostnames are case insensitive.
elif try_match_any_pattern(request_origin, origins, caseSensitive=False):
LOG.debug(
"The request's Origin header matches. Sending CORS headers.",
)
# Add a single Access-Control-Allow-Origin header, with either
# the value of the Origin header or the string "*" as value.
# -- W3Spec
return [request_origin]
else:
LOG.debug("The request's Origin header does not match any of allowed origins.")
return None
elif options.always_send:
if wildcard:
# If wildcard is in the origins, even if 'send_wildcard' is False,
# simply send the wildcard. Unless supports_credentials is True,
# since that is forbidden by the spec..
# It is the most-likely to be correct thing to do (the only other
# option is to return nothing, which almost certainly not what
# the developer wants if the '*' origin was specified.
if options.supports_credentials:
return None
else:
return ["*"]
else:
# Return all literal (non-regex) origins. After resolution a plain
# ``str`` entry is always a literal; regexes are compiled patterns.
return sorted(o for o in origins if isinstance(o, str))
# Terminate these steps, return the original request untouched.
else:
LOG.debug(
"The request did not contain an 'Origin' header. This means the browser or client did not request CORS, ensure the Origin Header is set."
)
return None
def get_allow_headers(options: _ComputedCorsOptions, acl_request_headers: str | None) -> str | None:
if acl_request_headers:
allow_headers = options.allow_headers
request_headers = [h.strip() for h in acl_request_headers.split(",")]
# any header that matches in the allow_headers
matching_headers = filter(
lambda h: try_match_any_pattern(h, allow_headers, caseSensitive=False),
request_headers,
)
return ", ".join(sorted(matching_headers))
return None
def get_cors_headers(
options: _ComputedCorsOptions, request_headers: Headers, request_method: str
) -> MultiDict[str, str | int | None]:
origins_to_set = get_cors_origins(options, request_headers.get("Origin"))
# Values are header values (str), with ``max_age`` allowing int and a few
# options allowing None; the trailing comprehension drops the None/empty
# entries before the result is returned.
headers: MultiDict[str, str | int | None] = MultiDict()
if not origins_to_set: # CORS is not enabled for this route
return headers
headers.setlist(ACL_ORIGIN, origins_to_set)
headers[ACL_EXPOSE_HEADERS] = options.expose_headers
if options.supports_credentials:
headers[ACL_CREDENTIALS] = "true" # case sensitive
if (
ACL_REQUEST_HEADER_PRIVATE_NETWORK in request_headers
and request_headers.get(ACL_REQUEST_HEADER_PRIVATE_NETWORK) == "true"
):
allow_private_network = "true" if options.allow_private_network else "false"
headers[ACL_RESPONSE_PRIVATE_NETWORK] = allow_private_network
# This is a preflight request
# http://www.w3.org/TR/cors/#resource-preflight-requests
if request_method == "OPTIONS":
acl_request_method = request_headers.get(ACL_REQUEST_METHOD, "").upper()
# If there is no Access-Control-Request-Method header or if parsing
# failed, do not set any additional headers
methods = options.methods
if acl_request_method and methods and acl_request_method in methods:
# If method is not a case-sensitive match for any of the values in
# list of methods do not set any additional headers and terminate
# this set of steps.
headers[ACL_ALLOW_HEADERS] = get_allow_headers(options, request_headers.get(ACL_REQUEST_HEADERS))
headers[ACL_MAX_AGE] = options.max_age
headers[ACL_METHODS] = methods
else:
LOG.info(
"The request's Access-Control-Request-Method header does not match allowed methods. CORS headers will not be applied."
)
# http://www.w3.org/TR/cors/#resource-implementation
if options.vary_header:
# Only set the header if the resolved origin can vary dynamically:
# i.e. we are not returning a wildcard, and more than one origin (or a
# regex) could have matched.
origins = options.origins
returns_wildcard = headers[ACL_ORIGIN] == "*"
if not returns_wildcard and (
len(origins) > 1 or len(origins_to_set) > 1 or any(isinstance(o, re.Pattern) for o in origins)
):
headers.add("Vary", "Origin")
return MultiDict((k, v) for k, v in headers.items() if v)
def set_cors_headers(resp: Response, options: _ComputedCorsOptions) -> Response:
"""
Performs the actual evaluation of Flask-CORS options and actually
modifies the response object.
This function is used both in the decorator and the after_request
callback
"""
# If CORS has already been evaluated via the decorator, skip
if hasattr(resp, FLASK_CORS_EVALUATED):
LOG.debug("CORS have been already evaluated, skipping")
return resp
# Some libraries, like OAuthlib, set resp.headers to non Multidict
# objects (Werkzeug Headers work as well). This is a problem because
# headers allow repeated values.
if not isinstance(resp.headers, Headers) and not isinstance(resp.headers, MultiDict):
# The non-standard header container is replaced with a MultiDict so
# repeated headers behave consistently.
resp.headers = MultiDict(resp.headers)
headers_to_set = get_cors_headers(options, request.headers, request.method)
LOG.debug("Settings CORS headers: %s", str(headers_to_set))
for k, v in headers_to_set.items():
resp.headers.add(k, v)
return resp
def probably_regex(maybe_regex: ResourcePattern) -> bool:
if isinstance(maybe_regex, re.Pattern):
return True
# Use characters common in regular expressions as a proxy for whether
# this string is in fact a regex.
return not _REGEX_HINT_CHARS.isdisjoint(maybe_regex)
def re_fix(reg: ResourcePattern) -> ResourcePattern:
"""
Replace the invalid regex r'*' with the valid, wildcard regex r'/.*' to
enable the CORS app extension to have a more user friendly api.
"""
return r".*" if reg == r"*" else reg
def try_match_any_pattern(inst: str, patterns: Iterable[ResourcePattern], caseSensitive: bool = True) -> bool:
return any(try_match_pattern(inst, pattern, caseSensitive) for pattern in patterns)
def try_match_pattern(value: Any, pattern: ResourcePattern, caseSensitive: bool = True) -> bool:
"""
Match a value against a *resolved* pattern: a compiled ``re.Pattern``
(whose case-sensitivity is already baked in) or a literal string. Used to
match request origins, headers, or paths; ``caseSensitive`` only affects
the literal comparison (origins and headers are case insensitive, paths are
case sensitive).
"""
if isinstance(pattern, re.Pattern):
return bool(pattern.match(value))
try:
v = str(value)
p = str(pattern)
except Exception:
return bool(value == pattern)
return v == p if caseSensitive else v.casefold() == p.casefold()
def merge_options(appInstance: Flask | Blueprint | None, *dicts: Mapping[str, Any]) -> dict[str, Any]:
"""
Merge the DEFAULT_OPTIONS, the app's configuration-specified options and any
dictionaries passed into a single raw options mapping. The last specified
option wins.
"""
options: dict[str, Any] = dict(DEFAULT_OPTIONS)
options.update(get_app_kwarg_dict(appInstance))
for d in dicts:
options.update(d)
return options
def get_cors_options(appInstance: Flask | Blueprint | None, *dicts: Mapping[str, Any]) -> _ComputedCorsOptions:
"""Compute the resolved CORS options for an application (see merge_options)."""
return serialize_options(merge_options(appInstance, *dicts))
def get_app_kwarg_dict(appInstance: Flask | Blueprint | None = None) -> dict[str, Any]:
"""Returns the dictionary of CORS specific app configurations."""
app = appInstance or current_app
# In order to support blueprints which do not have a config attribute
app_config = getattr(app, "config", {})
return {k.lower().replace("cors_", ""): value for k in CONFIG_OPTIONS if (value := app_config.get(k)) is not None}
def flexible_str(obj: Any) -> str | None:
"""
A more flexible str function which intelligently handles stringifying
strings, lists and other iterables. The results are lexographically sorted
to ensure generated responses are consistent when iterables such as Set
are used.
"""
if obj is None:
return None
elif not isinstance(obj, str) and isinstance(obj, Iterable):
return ", ".join(str(item) for item in sorted(obj))
else:
return str(obj)
def ensure_iterable(inst: Any) -> Iterable[Any]:
"""
Wraps scalars or string types as a list, or returns the iterable instance.
"""
if isinstance(inst, str) or not isinstance(inst, Iterable):
return [inst]
else:
return cast("Iterable[Any]", inst)
def sanitize_regex_param(param: Any) -> list[ResourcePattern]:
return [re_fix(x) for x in ensure_iterable(param)]
def _resolve_pattern(pattern: ResourcePattern, *, ignore_case: bool) -> ResourcePattern:
"""Resolve a single raw pattern once, at configuration time.
A regex-like string is compiled to a ``re.Pattern`` (with the correct
case-sensitivity baked in); a literal string and an already-compiled
pattern are returned unchanged. An invalid regular expression raises a
``ValueError`` so the misconfiguration fails fast at setup time rather than
silently never matching.
"""
if isinstance(pattern, str) and probably_regex(pattern):
try:
return re.compile(pattern, re.IGNORECASE if ignore_case else 0)
except re.error as exc:
raise ValueError(f"Invalid regular expression in Flask-CORS configuration: {pattern!r}") from exc
return pattern
def _resolve_patterns(patterns: Iterable[ResourcePattern], *, ignore_case: bool) -> list[ResourcePattern]:
"""Resolve a list of patterns (see :func:`_resolve_pattern`).
After this, the request path can simply test ``isinstance(p, re.Pattern)``
instead of re-running the :func:`probably_regex` heuristic on every request.
"""
return [_resolve_pattern(p, ignore_case=ignore_case) for p in patterns]
def serialize_options(opts: Mapping[str, Any]) -> _ComputedCorsOptions:
"""
Normalize a raw options mapping into a strongly-typed :class:`_ComputedCorsOptions`.
This is the single boundary where loosely-typed user input (arbitrary
keyword arguments, app config, resource dictionaries) is parsed into the
concrete, per-field types the rest of the package relies on.
"""
for key in opts:
if key not in _KNOWN_OPTIONS:
LOG.warning("Unknown option passed to Flask-CORS: %s", key)
# Ensure origins is a list of allowed origins with at least one entry. The
# wildcard is detected before the patterns are compiled (afterwards ``.*``
# is just another compiled regex, indistinguishable from a real one).
sanitized_origins = sanitize_regex_param(opts.get("origins"))
allow_all_origins = r".*" in sanitized_origins
supports_credentials = bool(opts.get("supports_credentials"))
send_wildcard = bool(opts.get("send_wildcard"))
# This is expressly forbidden by the spec. Raise a value error so people
# don't get burned in production.
if allow_all_origins and supports_credentials and send_wildcard:
raise ValueError(
"Cannot use supports_credentials in conjunction with"
"an origin string of '*'. See: "
"http://www.w3.org/TR/cors/#resource-requests"
)
origins = _resolve_patterns(sanitized_origins, ignore_case=True)
allow_headers = _resolve_patterns(sanitize_regex_param(opts.get("allow_headers")), ignore_case=True)
methods = flexible_str(opts.get("methods"))
if methods is not None:
methods = methods.upper()
max_age = opts.get("max_age")
if isinstance(max_age, timedelta):
max_age = str(int(max_age.total_seconds()))
return _ComputedCorsOptions(
origins=origins,
allow_headers=allow_headers,
allow_all_origins=allow_all_origins,
methods=methods,
expose_headers=flexible_str(opts.get("expose_headers")),
max_age=max_age,
supports_credentials=supports_credentials,
send_wildcard=send_wildcard,
automatic_options=bool(opts.get("automatic_options")),
vary_header=bool(opts.get("vary_header")),
intercept_exceptions=bool(opts.get("intercept_exceptions")),
always_send=bool(opts.get("always_send")),
allow_private_network=bool(opts.get("allow_private_network")),
)
@@ -0,0 +1,143 @@
from __future__ import annotations
import logging
from collections.abc import Callable
from functools import update_wrapper
from typing import TYPE_CHECKING, Any, TypeVar, cast
from flask import Response, current_app, make_response, request
from .core import FLASK_CORS_EVALUATED, CrossOriginOptionsInput, get_cors_options, set_cors_headers
if TYPE_CHECKING:
from typing_extensions import Unpack
LOG = logging.getLogger(__name__)
F = TypeVar("F", bound=Callable[..., Any])
def cross_origin(*args: Any, **kwargs: Unpack[CrossOriginOptionsInput]) -> Callable[[F], F]:
"""
This function is the decorator which is used to wrap a Flask route with.
In the simplest case, simply use the default parameters to allow all
origins in what is the most permissive configuration. If this method
modifies state or performs authentication which may be brute-forced, you
should add some degree of protection, such as Cross Site Request Forgery
protection.
:param origins:
The origin, or list of origins to allow requests from.
The origin(s) may be regular expressions, case-sensitive strings,
or else an asterisk
Default : '*'
:type origins: list, string or regex
:param methods:
The method or list of methods which the allowed origins are allowed to
access for non-simple requests.
Default : [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]
:type methods: list or string
:param expose_headers:
The header or list which are safe to expose to the API of a CORS API
specification.
Default : None
:type expose_headers: list or string
:param allow_headers:
The header or list of header field names which can be used when this
resource is accessed by allowed origins. The header(s) may be regular
expressions, case-sensitive strings, or else an asterisk.
Default : '*', allow all headers
:type allow_headers: list, string or regex
:param supports_credentials:
Allows users to make authenticated requests. If true, injects the
`Access-Control-Allow-Credentials` header in responses. This allows
cookies and credentials to be submitted across domains.
:note: This option cannot be used in conjunction with a '*' origin
Default : False
:type supports_credentials: bool
:param max_age:
The maximum time for which this CORS request maybe cached. This value
is set as the `Access-Control-Max-Age` header.
Default : None
:type max_age: timedelta, integer, string or None
:param send_wildcard: If True, and the origins parameter is `*`, a wildcard
`Access-Control-Allow-Origin` header is sent, rather than the
request's `Origin` header.
Default : False
:type send_wildcard: bool
:param vary_header:
If True, the header Vary: Origin will be returned as per the W3
implementation guidelines.
Setting this header when the `Access-Control-Allow-Origin` is
dynamically generated (e.g. when there is more than one allowed
origin, and an Origin than '*' is returned) informs CDNs and other
caches that the CORS headers are dynamic, and cannot be cached.
If False, the Vary header will never be injected or altered.
Default : True
:type vary_header: bool
:param automatic_options:
Only applies to the `cross_origin` decorator. If True, Flask-CORS will
override Flask's default OPTIONS handling to return CORS headers for
OPTIONS requests.
Default : True
:type automatic_options: bool
"""
_options = kwargs
def decorator(f: F) -> F:
LOG.debug("Enabling %s for cross_origin using options:%s", f, _options)
# If True, intercept OPTIONS requests by modifying the view function,
# replicating Flask's default behavior, and wrapping the response with
# CORS headers.
#
# If f.provide_automatic_options is unset or True, Flask's route
# decorator (which is actually wraps the function object we return)
# intercepts OPTIONS handling, and requests will not have CORS headers
if _options.get("automatic_options", True):
# ``f`` is a plain view function; these attributes are read by
# Flask's routing. Use an ``Any`` alias so the dynamic attribute
# assignment type-checks.
view_func: Any = f
required_methods = set(getattr(f, "required_methods", set()))
required_methods.add("OPTIONS")
view_func.required_methods = required_methods
view_func.provide_automatic_options = False
def wrapped_function(*args: Any, **kwargs: Any) -> Response:
# Handle setting of Flask-Cors parameters
options = get_cors_options(current_app, _options)
if options.automatic_options and request.method == "OPTIONS":
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
set_cors_headers(resp, options)
setattr(resp, FLASK_CORS_EVALUATED, True)
return resp
return cast(F, update_wrapper(wrapped_function, f))
return decorator
@@ -0,0 +1,237 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from urllib.parse import unquote
from flask import Blueprint, Flask, Response, request
from .core import (
ACL_ORIGIN,
CorsOptionsInput,
ResourcePattern,
_ComputedCorsOptions,
get_cors_options,
get_regexp_pattern,
merge_options,
parse_resources,
serialize_options,
set_cors_headers,
try_match_pattern,
)
if TYPE_CHECKING:
from collections.abc import Callable
from typing_extensions import Unpack
LOG = logging.getLogger(__name__)
class CORS:
"""
Initializes Cross Origin Resource sharing for the application. The
arguments are identical to `cross_origin`, with the addition of a
`resources` parameter. The resources parameter defines a series of regular
expressions for resource paths to match and optionally, the associated
options to be applied to the particular resource. These options are
identical to the arguments to `cross_origin`.
The settings for CORS are determined in the following order
1. Resource level settings (e.g when passed as a dictionary)
2. Keyword argument settings
3. App level configuration settings (e.g. CORS_*)
4. Default settings
Note: as it is possible for multiple regular expressions to match a
resource path, the regular expressions are first sorted by length,
from longest to shortest, in order to attempt to match the most
specific regular expression. This allows the definition of a
number of specific resource options, with a wildcard fallback
for all other resources.
:param resources:
The series of regular expression and (optionally) associated CORS
options to be applied to the given resource path.
If the argument is a dictionary, it's keys must be regular expressions,
and the values must be a dictionary of kwargs, identical to the kwargs
of this function.
If the argument is a list, it is expected to be a list of regular
expressions, for which the app-wide configured options are applied.
If the argument is a string, it is expected to be a regular expression
for which the app-wide configured options are applied.
Default : Match all and apply app-level configuration
:type resources: dict, iterable or string
:param origins:
The origin, or list of origins to allow requests from.
The origin(s) may be regular expressions, case-sensitive strings,
or else an asterisk.
.. note::
origins must include the schema and the port (if not port 80),
e.g.,
`CORS(app, origins=["http://localhost:8000", "https://example.com"])`.
Default : '*'
:type origins: list, string or regex
:param methods:
The method or list of methods which the allowed origins are allowed to
access for non-simple requests.
Default : [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]
:type methods: list or string
:param expose_headers:
The header or list which are safe to expose to the API of a CORS API
specification.
Default : None
:type expose_headers: list or string
:param allow_headers:
The header or list of header field names which can be used when this
resource is accessed by allowed origins. The header(s) may be regular
expressions, case-sensitive strings, or else an asterisk.
Default : '*', allow all headers
:type allow_headers: list, string or regex
:param supports_credentials:
Allows users to make authenticated requests. If true, injects the
`Access-Control-Allow-Credentials` header in responses. This allows
cookies and credentials to be submitted across domains.
:note: This option cannot be used in conjunction with a '*' origin
Default : False
:type supports_credentials: bool
:param max_age:
The maximum time for which this CORS request maybe cached. This value
is set as the `Access-Control-Max-Age` header.
Default : None
:type max_age: timedelta, integer, string or None
:param send_wildcard: If True, and the origins parameter is `*`, a wildcard
`Access-Control-Allow-Origin` header is sent, rather than the
request's `Origin` header.
Default : False
:type send_wildcard: bool
:param vary_header:
If True, the header Vary: Origin will be returned as per the W3
implementation guidelines.
Setting this header when the `Access-Control-Allow-Origin` is
dynamically generated (e.g. when there is more than one allowed
origin, and an Origin than '*' is returned) informs CDNs and other
caches that the CORS headers are dynamic, and cannot be cached.
If False, the Vary header will never be injected or altered.
Default : True
:type vary_header: bool
:param allow_private_network:
If True, the response header `Access-Control-Allow-Private-Network`
will be set with the value 'true' whenever the request header
`Access-Control-Request-Private-Network` has a value 'true'.
If False, the response header `Access-Control-Allow-Private-Network`
will be set with the value 'false' whenever the request header
`Access-Control-Request-Private-Network` has a value of 'true'.
If the request header `Access-Control-Request-Private-Network` is
not present or has a value other than 'true', the response header
`Access-Control-Allow-Private-Network` will not be set.
Default : True
:type allow_private_network: bool
"""
def __init__(self, app: Flask | Blueprint | None = None, **kwargs: Unpack[CorsOptionsInput]) -> None:
self._options = kwargs
if app is not None:
self.init_app(app, **kwargs)
def init_app(self, app: Flask | Blueprint, **kwargs: Unpack[CorsOptionsInput]) -> None:
# The resources and options may be specified in the App Config, the CORS constructor
# or the kwargs to the call to init_app.
merged = merge_options(app, self._options, kwargs)
options = serialize_options(merged)
# Flatten our resources into a list of the form
# (pattern_or_regexp, dictionary_of_raw_options). ``resources`` is a raw
# spec consumed only here, so it is read from the merged mapping rather
# than carried on the resolved options.
resources = parse_resources(merged.get("resources", r"/*"))
# Compute the options for each resource from the raw inputs (defaults,
# app config, constructor kwargs, init_app kwargs) plus the per-resource
# overrides. Re-merging the raw inputs rather than the already-resolved
# ``options`` avoids re-normalizing compiled regexes.
resolved_resources: list[tuple[ResourcePattern, _ComputedCorsOptions]] = [
(pattern, get_cors_options(app, self._options, kwargs, opts)) for (pattern, opts) in resources
]
# Create a human-readable form of these resources by converting the compiled
# regular expressions into strings.
resources_human = {get_regexp_pattern(pattern): opts for (pattern, opts) in resolved_resources}
LOG.debug("Configuring CORS with resources: %s", resources_human)
cors_after_request = make_after_request_function(resolved_resources)
app.after_request(cors_after_request)
# Wrap exception handlers with cross_origin so error responses also
# receive CORS headers. Blueprints have no ``handle_exception`` /
# ``make_response``, so the ``hasattr`` guard skips them; the ``Any``
# alias lets us read and reassign those Flask-only methods without
# tripping mypy's attr-defined / method-assign checks for ``Blueprint``.
if options.intercept_exceptions and hasattr(app, "handle_exception"):
app_any: Any = app
def _after_request_decorator(f: Callable[..., Any]) -> Callable[..., Response]:
def wrapped_function(*args: Any, **kwargs: Any) -> Response:
return cors_after_request(app_any.make_response(f(*args, **kwargs)))
return wrapped_function
app_any.handle_exception = _after_request_decorator(app_any.handle_exception)
app_any.handle_user_exception = _after_request_decorator(app_any.handle_user_exception)
def make_after_request_function(
resources: list[tuple[ResourcePattern, _ComputedCorsOptions]],
) -> Callable[[Response], Response]:
def cors_after_request(resp: Response) -> Response:
# If CORS headers are set in a view decorator, pass
if resp.headers is not None and resp.headers.get(ACL_ORIGIN):
LOG.debug("CORS have been already evaluated, skipping")
return resp
normalized_path = unquote(request.path)
for res_regex, res_options in resources:
if try_match_pattern(normalized_path, res_regex, caseSensitive=True):
LOG.debug(
"Request to '%r' matches CORS resource '%s'. Using options: %s",
request.path,
get_regexp_pattern(res_regex),
res_options,
)
set_cors_headers(resp, res_options)
break
else:
LOG.debug("No CORS rule matches")
return resp
return cors_after_request