extension.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import logging
  2. from urllib.parse import unquote_plus
  3. from flask import request
  4. from .core import ACL_ORIGIN, get_cors_options, get_regexp_pattern, parse_resources, set_cors_headers, try_match
  5. LOG = logging.getLogger(__name__)
  6. class CORS:
  7. """
  8. Initializes Cross Origin Resource sharing for the application. The
  9. arguments are identical to `cross_origin`, with the addition of a
  10. `resources` parameter. The resources parameter defines a series of regular
  11. expressions for resource paths to match and optionally, the associated
  12. options to be applied to the particular resource. These options are
  13. identical to the arguments to `cross_origin`.
  14. The settings for CORS are determined in the following order
  15. 1. Resource level settings (e.g when passed as a dictionary)
  16. 2. Keyword argument settings
  17. 3. App level configuration settings (e.g. CORS_*)
  18. 4. Default settings
  19. Note: as it is possible for multiple regular expressions to match a
  20. resource path, the regular expressions are first sorted by length,
  21. from longest to shortest, in order to attempt to match the most
  22. specific regular expression. This allows the definition of a
  23. number of specific resource options, with a wildcard fallback
  24. for all other resources.
  25. :param resources:
  26. The series of regular expression and (optionally) associated CORS
  27. options to be applied to the given resource path.
  28. If the argument is a dictionary, it's keys must be regular expressions,
  29. and the values must be a dictionary of kwargs, identical to the kwargs
  30. of this function.
  31. If the argument is a list, it is expected to be a list of regular
  32. expressions, for which the app-wide configured options are applied.
  33. If the argument is a string, it is expected to be a regular expression
  34. for which the app-wide configured options are applied.
  35. Default : Match all and apply app-level configuration
  36. :type resources: dict, iterable or string
  37. :param origins:
  38. The origin, or list of origins to allow requests from.
  39. The origin(s) may be regular expressions, case-sensitive strings,
  40. or else an asterisk.
  41. .. note::
  42. origins must include the schema and the port (if not port 80),
  43. e.g.,
  44. `CORS(app, origins=["http://localhost:8000", "https://example.com"])`.
  45. Default : '*'
  46. :type origins: list, string or regex
  47. :param methods:
  48. The method or list of methods which the allowed origins are allowed to
  49. access for non-simple requests.
  50. Default : [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]
  51. :type methods: list or string
  52. :param expose_headers:
  53. The header or list which are safe to expose to the API of a CORS API
  54. specification.
  55. Default : None
  56. :type expose_headers: list or string
  57. :param allow_headers:
  58. The header or list of header field names which can be used when this
  59. resource is accessed by allowed origins. The header(s) may be regular
  60. expressions, case-sensitive strings, or else an asterisk.
  61. Default : '*', allow all headers
  62. :type allow_headers: list, string or regex
  63. :param supports_credentials:
  64. Allows users to make authenticated requests. If true, injects the
  65. `Access-Control-Allow-Credentials` header in responses. This allows
  66. cookies and credentials to be submitted across domains.
  67. :note: This option cannot be used in conjunction with a '*' origin
  68. Default : False
  69. :type supports_credentials: bool
  70. :param max_age:
  71. The maximum time for which this CORS request maybe cached. This value
  72. is set as the `Access-Control-Max-Age` header.
  73. Default : None
  74. :type max_age: timedelta, integer, string or None
  75. :param send_wildcard: If True, and the origins parameter is `*`, a wildcard
  76. `Access-Control-Allow-Origin` header is sent, rather than the
  77. request's `Origin` header.
  78. Default : False
  79. :type send_wildcard: bool
  80. :param vary_header:
  81. If True, the header Vary: Origin will be returned as per the W3
  82. implementation guidelines.
  83. Setting this header when the `Access-Control-Allow-Origin` is
  84. dynamically generated (e.g. when there is more than one allowed
  85. origin, and an Origin than '*' is returned) informs CDNs and other
  86. caches that the CORS headers are dynamic, and cannot be cached.
  87. If False, the Vary header will never be injected or altered.
  88. Default : True
  89. :type vary_header: bool
  90. :param allow_private_network:
  91. If True, the response header `Access-Control-Allow-Private-Network`
  92. will be set with the value 'true' whenever the request header
  93. `Access-Control-Request-Private-Network` has a value 'true'.
  94. If False, the response header `Access-Control-Allow-Private-Network`
  95. will be set with the value 'false' whenever the request header
  96. `Access-Control-Request-Private-Network` has a value of 'true'.
  97. If the request header `Access-Control-Request-Private-Network` is
  98. not present or has a value other than 'true', the response header
  99. `Access-Control-Allow-Private-Network` will not be set.
  100. Default : True
  101. :type allow_private_network: bool
  102. """
  103. def __init__(self, app=None, **kwargs):
  104. self._options = kwargs
  105. if app is not None:
  106. self.init_app(app, **kwargs)
  107. def init_app(self, app, **kwargs):
  108. # The resources and options may be specified in the App Config, the CORS constructor
  109. # or the kwargs to the call to init_app.
  110. options = get_cors_options(app, self._options, kwargs)
  111. # Flatten our resources into a list of the form
  112. # (pattern_or_regexp, dictionary_of_options)
  113. resources = parse_resources(options.get("resources"))
  114. # Compute the options for each resource by combining the options from
  115. # the app's configuration, the constructor, the kwargs to init_app, and
  116. # finally the options specified in the resources dictionary.
  117. resources = [(pattern, get_cors_options(app, options, opts)) for (pattern, opts) in resources]
  118. # Create a human-readable form of these resources by converting the compiled
  119. # regular expressions into strings.
  120. resources_human = {get_regexp_pattern(pattern): opts for (pattern, opts) in resources}
  121. LOG.debug("Configuring CORS with resources: %s", resources_human)
  122. cors_after_request = make_after_request_function(resources)
  123. app.after_request(cors_after_request)
  124. # Wrap exception handlers with cross_origin
  125. # These error handlers will still respect the behavior of the route
  126. if options.get("intercept_exceptions", True):
  127. def _after_request_decorator(f):
  128. def wrapped_function(*args, **kwargs):
  129. return cors_after_request(app.make_response(f(*args, **kwargs)))
  130. return wrapped_function
  131. if hasattr(app, "handle_exception"):
  132. app.handle_exception = _after_request_decorator(app.handle_exception)
  133. app.handle_user_exception = _after_request_decorator(app.handle_user_exception)
  134. def make_after_request_function(resources):
  135. def cors_after_request(resp):
  136. # If CORS headers are set in a view decorator, pass
  137. if resp.headers is not None and resp.headers.get(ACL_ORIGIN):
  138. LOG.debug("CORS have been already evaluated, skipping")
  139. return resp
  140. normalized_path = unquote_plus(request.path)
  141. for res_regex, res_options in resources:
  142. if try_match(normalized_path, res_regex):
  143. LOG.debug(
  144. "Request to '%r' matches CORS resource '%s'. Using options: %s",
  145. request.path,
  146. get_regexp_pattern(res_regex),
  147. res_options,
  148. )
  149. set_cors_headers(resp, res_options)
  150. break
  151. else:
  152. LOG.debug("No CORS rule matches")
  153. return resp
  154. return cors_after_request