Repository: pallets/werkzeug

Issue:
Support CORS
It would be nice to support CORS [1][2] 

even static files need to support it

[1] http://www.w3.org/TR/cors/
[2] http://enable-cors.org/



Discussion:
Werkzeug already supports CORS in that you can set the headers on the responses you return in your WSGI application. I'm not sure what else can or should be done within Werkzeug to "support CORS", can you elaborate on that?

I'm not the OP, but since CORS is pretty much a boilerplate header injection, it might perhaps be a good idea to have it as a setting/parameter.  Something along the lines of [this snippet](http://www.nullege.com/codes/show/src%40j%40o%40jokk-0.1%40jokk%40server.py/124/werkzeug.wrappers.Response.headers.add/python), although here-notably there is no OPTIONS, so the preflight will be problematic...

```
    if 'cors' in config and config['cors'] is True:
        response.headers.add('Access-Control-Allow-Origin', '*')
        response.headers.add('Access-Control-Allow-Methods',
                             'GET,PUT,POST,DELETE,PATCH')
        response.headers.add('Access-Control-Allow-Headers',
                             'Content-Type, Authorization')
```

...you know the python mantra: "batteries included"... ;)

But CORS is more than allowing `*`, especially when you use cookies and stuff. Personally, I use a 20 LoC WSGI middleware to add CORS headers and answer preflight request. It's not that hard.

@posativ - Care to share on a gist maybe? :)

In a very limited form:

``` python
from werkzeug.datastructures import Headers

class CORSMiddleware(object):
    """Add Cross-origin resource sharing headers to every request."""

    def __init__(self, app, origin):
        self.app = app
        self.origin = origin

    def __call__(self, environ, start_response):

        def add_cors_headers(status, headers, exc_info=None):
            headers = Headers(headers)
            headers.add("Access-Control-Allow-Origin", self.origin)
            headers.add("Access-Control-Allow-Headers", "Origin, ...")
            headers.add("Access-Control-Allow-Credentials", "true")
            headers.add("Access-Control-Allow-Methods", "GET, POST, PUT, etc")
            headers.add("Access-Control-Expose-Headers", "...")
            return start_response(status, headers.to_list(), exc_info)

        if environ.get("REQUEST_METHOD") == "OPTIONS":
            add_cors_headers("200 Ok", [("Content-Type", "text/plain")])
            return [b'200 Ok']

        return self.app(environ, add_cors_headers)

# usage
app = CORSMiddleware(make_app(...), "http://example.org/")
```

You may extend this class to include custom headers, credentials, wild-cards.

@posativ - Thank you for this, I'll try it early next week when I'll resume work on that project. :)

Need to bump this up.

Relative question http://stackoverflow.com/questions/19382431/set-header-on-werkzeug-exception

Related line: https://github.com/pallets/werkzeug/blob/master/werkzeug/debug/__init__.py#L297

the `start_response('500 INTERNAL SERVER ERROR',` makes it hard to add CORS header like `Access-Control-Allow-Origin: *`. But curiously we already have `X-XSS-Protection: 0` there.

I have Flask-CORS enabled, been debugging a tricky remote ajax API call in Chrome, when Flask has an exception, I can not see the debug content because of CORS. Can we add this header in debug model?

Why would you want to set a CORS header on the debugger? And why can't you do that with a wrapping middleware?

Also I want to close this since I don't think we need to add every middleware to werkzeug.

@unititaker

> Why would you want to set a CORS header on the debugger?

Because it's a tricky scenario invoked by cross-domain AJAX request, in can only debug it in Chrome DevTool after series of javascript event triggering, if no CORS header, the Chrome DevTool does not display any content at all. I cant see nothing. I can only stare at a blank page with 500 server error.

> And why can't you do that with a wrapping middleware?

 because wrapped middleware works with only **NORMAL** (proper returned Flask application ) pages, but does not work at all when Werkzeug exception debugger happens.

Here's the "wrapping middleware" you referring to, https://github.com/corydolphin/flask-cors/issues/67 you can see we cant do much about it unless lower level Werkzeug stuff was tweaked.

I presume @untitaker did not take a look at the source code I linked? Here's a screenshot:

<img width="676" alt="qq20160420-3" src="https://cloud.githubusercontent.com/assets/987524/14660612/72b5a128-06d9-11e6-8fd3-720cf3d417bc.png">

@lambdaq now you are being mean to @untitaker 

any middleware that does wrap around a debug wrapped application can change the start_response behaviour - even for the debugger - so the code you pasted and linked seems completely irrelevant,
since a cors middle-ware wrapping around could just change it

perhaps im missing your exact problem, due to different perspective/understanding please outline  the exact use-case that seems broken/impossible 

The situation is made a bit tricky because the debugger middleware is applied when calling app.run. Still you can work around this by applying both your CORS and the debugger middleware manually. IMO this is fine for such an edgecase, but if you have concrete proposals for changing the debugger middleware in a way that doesn't compromise security, please do suggest them. I don't know of any easy changes though.

Also I am well aware of the code in Werkzeug. I guess that's why I'm maintaining it?

sorry, I apologize for my language, Werkzeug is awesome project. 

I guess I just have to monkey edit these `start_response()` lines and add CORS header bruteforce for my dev environment, it's more convenient than a hairy middleware.

Also you might consider adding extra JS code instead that redirects to the debugger if a 500 response comes back.

@untitaker JS code can do nothing, if there's 500 error, you can only tell its http status code is 500, the traceback from the debugger page is what I am after.

also the js code is hairy compiled reactjs with tons of stuff, developed by another team, it takes 300s+ to `npm run dev`. Not easy to tweak or add few debug lines.


Resolving patch:
diff --git a/src/werkzeug/wrappers/cors.py b/src/werkzeug/wrappers/cors.py
new file mode 100644
--- /dev/null
+++ b/src/werkzeug/wrappers/cors.py
@@ -0,0 +1,102 @@
+from ..http import dump_header
+from ..http import parse_set_header
+from ..utils import environ_property
+from ..utils import header_property
+
+
+class CORSRequestMixin(object):
+    """A mixin for :class:`~werkzeug.wrappers.BaseRequest` subclasses
+    that adds descriptors for Cross Origin Resource Sharing (CORS)
+    headers.
+
+    .. versionadded:: 1.0
+    """
+
+    origin = environ_property(
+        "HTTP_ORIGIN",
+        doc=(
+            "The host that the request originated from. Set"
+            " :attr:`~CORSResponseMixin.access_control_allow_origin` on"
+            " the response to indicate which origins are allowed."
+        ),
+    )
+
+    access_control_request_headers = environ_property(
+        "HTTP_ACCESS_CONTROL_REQUEST_HEADERS",
+        load_func=parse_set_header,
+        doc=(
+            "Sent with a preflight request to indicate which headers"
+            " will be sent with the cross origin request. Set"
+            " :attr:`~CORSResponseMixin.access_control_allow_headers`"
+            " on the response to indicate which headers are allowed."
+        ),
+    )
+
+    access_control_request_method = environ_property(
+        "HTTP_ACCESS_CONTROL_REQUEST_METHOD",
+        doc=(
+            "Sent with a preflight request to indicate which method"
+            " will be used for the cross origin request. Set"
+            " :attr:`~CORSResponseMixin.access_control_allow_methods`"
+            " on the response to indicate which methods are allowed."
+        ),
+    )
+
+
+class CORSResponseMixin(object):
+    """A mixin for :class:`~werkzeug.wrappers.BaseResponse` subclasses
+    that adds descriptors for Cross Origin Resource Sharing (CORS)
+    headers.
+
+    .. versionadded:: 1.0
+    """
+
+    @property
+    def access_control_allow_credentials(self):
+        """Whether credentials can be shared by the browser to
+        JavaScript code. As part of the preflight request it indicates
+        whether credentials can be used on the cross origin request.
+        """
+        return "Access-Control-Allow-Credentials" in self.headers
+
+    @access_control_allow_credentials.setter
+    def access_control_allow_credentials(self, value):
+        if value is True:
+            self.headers["Access-Control-Allow-Credentials"] = "true"
+        else:
+            self.headers.pop("Access-Control-Allow-Credentials", None)
+
+    access_control_allow_headers = header_property(
+        "Access-Control-Allow-Headers",
+        load_func=parse_set_header,
+        dump_func=dump_header,
+        doc="Which headers can be sent with the cross origin request.",
+    )
+
+    access_control_allow_methods = header_property(
+        "Access-Control-Allow-Methods",
+        load_func=parse_set_header,
+        dump_func=dump_header,
+        doc="Which methods can be used for the cross origin request.",
+    )
+
+    access_control_allow_origin = header_property(
+        "Access-Control-Allow-Origin",
+        load_func=parse_set_header,
+        dump_func=dump_header,
+        doc="The origins that may make cross origin requests.",
+    )
+
+    access_control_expose_headers = header_property(
+        "Access-Control-Expose-Headers",
+        load_func=parse_set_header,
+        dump_func=dump_header,
+        doc="Which headers can be shared by the browser to JavaScript code.",
+    )
+
+    access_control_max_age = header_property(
+        "Access-Control-Max-Age",
+        load_func=int,
+        dump_func=str,
+        doc="The maximum age in seconds the access control settings can be cached for.",
+    )
diff --git a/src/werkzeug/wrappers/request.py b/src/werkzeug/wrappers/request.py
--- a/src/werkzeug/wrappers/request.py
+++ b/src/werkzeug/wrappers/request.py
@@ -2,6 +2,7 @@
 from .auth import AuthorizationMixin
 from .base_request import BaseRequest
 from .common_descriptors import CommonRequestDescriptorsMixin
+from .cors import CORSRequestMixin
 from .etag import ETagRequestMixin
 from .user_agent import UserAgentMixin
 
@@ -12,15 +13,19 @@ class Request(
     ETagRequestMixin,
     UserAgentMixin,
     AuthorizationMixin,
+    CORSRequestMixin,
     CommonRequestDescriptorsMixin,
 ):
     """Full featured request object implementing the following mixins:
 
-    - :class:`AcceptMixin` for accept header parsing
-    - :class:`ETagRequestMixin` for etag and cache control handling
-    - :class:`UserAgentMixin` for user agent introspection
-    - :class:`AuthorizationMixin` for http auth handling
-    - :class:`CommonRequestDescriptorsMixin` for common headers
+    -   :class:`AcceptMixin` for accept header parsing
+    -   :class:`ETagRequestMixin` for etag and cache control handling
+    -   :class:`UserAgentMixin` for user agent introspection
+    -   :class:`AuthorizationMixin` for http auth handling
+    -   :class:`~werkzeug.wrappers.cors.CORSRequestMixin` for Cross
+        Origin Resource Sharing headers
+    -   :class:`CommonRequestDescriptorsMixin` for common headers
+
     """
 
 
diff --git a/src/werkzeug/wrappers/response.py b/src/werkzeug/wrappers/response.py
--- a/src/werkzeug/wrappers/response.py
+++ b/src/werkzeug/wrappers/response.py
@@ -2,6 +2,7 @@
 from .auth import WWWAuthenticateMixin
 from .base_response import BaseResponse
 from .common_descriptors import CommonResponseDescriptorsMixin
+from .cors import CORSResponseMixin
 from .etag import ETagResponseMixin
 
 
@@ -65,14 +66,19 @@ def stream(self):
 class Response(
     BaseResponse,
     ETagResponseMixin,
+    WWWAuthenticateMixin,
+    CORSResponseMixin,
     ResponseStreamMixin,
     CommonResponseDescriptorsMixin,
-    WWWAuthenticateMixin,
 ):
     """Full featured response object implementing the following mixins:
 
-    - :class:`ETagResponseMixin` for etag and cache control handling
-    - :class:`ResponseStreamMixin` to add support for the `stream` property
-    - :class:`CommonResponseDescriptorsMixin` for various HTTP descriptors
-    - :class:`WWWAuthenticateMixin` for HTTP authentication support
+    -   :class:`ETagResponseMixin` for etag and cache control handling
+    -   :class:`WWWAuthenticateMixin` for HTTP authentication support
+    -   :class:`~werkzeug.wrappers.cors.CORSResponseMixin` for Cross
+        Origin Resource Sharing headers
+    -   :class:`ResponseStreamMixin` to add support for the ``stream``
+        property
+    -   :class:`CommonResponseDescriptorsMixin` for various HTTP
+        descriptors
     """


Test patch:
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -270,6 +270,31 @@ def failing_application(request):
     assert resp.status_code == 400
 
 
+def test_request_access_control():
+    request = wrappers.Request.from_values(
+        headers={
+            "Origin": "https://palletsprojects.com",
+            "Access-Control-Request-Headers": "X-A, X-B",
+            "Access-Control-Request-Method": "PUT",
+        },
+    )
+    assert request.origin == "https://palletsprojects.com"
+    assert request.access_control_request_headers == {"X-A", "X-B"}
+    assert request.access_control_request_method == "PUT"
+
+
+def test_response_access_control():
+    response = wrappers.Response("Hello World")
+    assert response.access_control_allow_credentials is False
+    response.access_control_allow_credentials = True
+    response.access_control_allow_headers = ["X-A", "X-B"]
+    assert response.headers["Access-Control-Allow-Credentials"] == "true"
+    assert set(response.headers["Access-Control-Allow-Headers"].split(", ")) == {
+        "X-A",
+        "X-B",
+    }
+
+
 def test_base_response():
     # unicode
     response = wrappers.BaseResponse(u"öäü")