├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin └── run_tests.sh ├── cors ├── __init__.py ├── constants.py ├── cors_application.py ├── cors_handler.py ├── cors_options.py ├── errors.py ├── filters.py ├── http_response.py └── validators.py ├── examples ├── __init__.py └── app-engine │ ├── .gitignore │ ├── __init__.py │ ├── app.yaml │ └── main.py ├── setup.py └── tests ├── __init__.py ├── test_cors_handler.py ├── test_cors_options.py ├── test_filters.py └── test_validators.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *~ 4 | *.swp 5 | *.pyc 6 | *.idea 7 | *.egg* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | Copyright (c) 2012 Monsur Hossain 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * 2 | recursive-exclude examples * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cors-python 2 | 3 | A server-side CORS implementation for Python. This library is an early alpha 4 | release. Testing and feedback welcome! 5 | 6 | # Details 7 | 8 | CORS can easily be enabled by setting the `Access-Control-Allow-Origin: *` 9 | response header. And for many resources that is sufficient. But if your API goes 10 | beyond simple requests, there are many edge cases that can make CORS 11 | difficult to work with. 12 | 13 | This library aims to make CORS easy: give it your request info, and 14 | it spits out the CORS response headers. All the details of handling preflights 15 | are managed for you. Its features include: 16 | 17 | * Configure all aspects of CORS. Make your API as open as you like. 18 | * Custom validators give you control over exactly which origins, methods and headers are allowed. 19 | * Ease-of-use. Sits on top of your application with ~ 2 lines of code (see example below). 20 | * Errors can either fire immediately or pass through to your app. 21 | * Supports the "Vary" header, which is sometimes needed to avoid caching by proxy servers. 22 | * Can be adapted to work with most apps. 23 | 24 | # Installation 25 | 26 | Nothing fancy for now, just copy the cors/ directory over. 27 | 28 | # Usage 29 | 30 | See the app engine app under the "examples" directory for a sample usage. 31 | 32 | For WSGI-compatible apps, you can wrap you application with the 33 | cors_application.py middleware: 34 | 35 | webapp = webapp2.WSGIApplication([('/', MainHandler)]) 36 | corsapp = CorsApplication(webapp, CorsOptions()) 37 | 38 | The CorsOptions class accepts the following properties: 39 | 40 | _allow\_origins_ (Validator) - The origins that are allowed. Set to True to 41 | allow all origins, or to a list of valid origins. Defaults to True, which allows 42 | all origins, and appends the `Access-Control-Allow-Origin: *` response header. 43 | 44 | _allow\_credentials_ (bool) - Whether or not the app supports credentials. If 45 | True, appends the `Access-Control-Allow-Credentials: true` header. Defaults to 46 | False. 47 | 48 | _allow\_methods_ (Validator) - The HTTP methods that are allowed. Set to True to 49 | allow all methods, or to a list of allowed methods. Defauts to ['HEAD', 'GET', 50 | 'PUT', 'POST', 'DELETE'], which appends the 51 | `Access-Control-Allow-Methods: HEAD, GET, PUT, POST, DELETE` response header. 52 | 53 | _allow\_headers_ (Validator) - The HTTP request headers that are allowed. Set to 54 | True to allow all headers, or to a list of allowed headers. Defaults to True, 55 | which appends the `Access-Control-Allow-Headers` response header. 56 | 57 | _expose\_headers_ (list of strings) - List of response headers to expose to the 58 | client. Defaults to None. Appends the `Access-Control-Expose-Headers` response 59 | header. 60 | 61 | _max\_age_ (int) - The maximum time (in seconds) to cache the preflight 62 | response. Defaults to None, which doesn't append any response headers. Appends 63 | the `Access-Control-Max-Age` header when set. 64 | 65 | _vary_ (bool) - Set to True if the `Vary: Origin` header should be appended to 66 | the response, False otherwise. The `Vary` header is useful when used in 67 | conjunction with a list of valid origins, and tells downstream proxy servers 68 | not to cache the response based on Origin. The default value is False for '*' 69 | origins, True otherwise. 70 | 71 | _allow\_non\_cors\_requests_ (bool) - Whether non-CORS requests should be 72 | allowed. Defaults to True. 73 | 74 | _continue\_on\_error_ (bool) - Whether an invalid CORS request should trigger 75 | an error, or continue processing. Defaults to False. 76 | 77 | ## Validators 78 | 79 | A few options above are marked as the special type "Validator". This type is 80 | used to validate the origin, http method, and header values. The actual type 81 | of the property can be set to various values, depending on the need: 82 | 83 | * Boolean: A value of True indicates that all values are allowed. A value 84 | of False indicates that no value is allowed. 85 | 86 | * List of strings: The list of valid values. For example, the default list of 87 | HTTP methods is ['HEAD', 'GET', 'PUT', 'POST', 'DELETE']. 88 | 89 | * Regex (coming soon) - A regular expression to validate the value. Could be 90 | useful for validating a set of subdomains (i.e. http://.*\.foo\.com) or custom 91 | headers (i.e. x-prefix-.*) 92 | 93 | * Function (coming soon) - Allows you to write your own function to validate the 94 | input. 95 | 96 | # Integrating with your own app 97 | 98 | If the WSGI middleware does not meet your needs, you can always integrate with 99 | the CORS library by writing your own handler. Your handler should call the 100 | CorsHandler class in order to do the heavy lifting. See 101 | [cors_application.py](https://github.com/monsur/cors-python/blob/master/cors/cors_application.py) 102 | for an example of how to integrate with this library. 103 | -------------------------------------------------------------------------------- /bin/run_tests.sh: -------------------------------------------------------------------------------- 1 | ls -1 tests/test_*.py | xargs -L1 python 2 | -------------------------------------------------------------------------------- /cors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsur/cors-python/9eb64ae395f0685ba957e267a4512eeb44752830/cors/__init__.py -------------------------------------------------------------------------------- /cors/constants.py: -------------------------------------------------------------------------------- 1 | ORIGIN = 'Origin' 2 | VARY = 'Vary' 3 | ACCESS_CONTROL_REQUEST_METHOD = 'Access-Control-Request-Method' 4 | ACCESS_CONTROL_REQUEST_HEADERS = 'Access-Control-Request-Headers' 5 | ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' 6 | ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials' 7 | ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods' 8 | ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers' 9 | ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age' 10 | ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers' 11 | -------------------------------------------------------------------------------- /cors/cors_application.py: -------------------------------------------------------------------------------- 1 | """WSGI middleware for handling CORS requests.""" 2 | 3 | import webob 4 | 5 | from cors import cors_handler 6 | from cors import http_response 7 | 8 | 9 | class CorsApplication(object): 10 | """WSGI middleware for handling CORS requests.""" 11 | 12 | def __init__(self, app, options=None): 13 | self._handler = cors_handler.CorsHandler(options) 14 | self.app = app 15 | 16 | @property 17 | def handler(self): 18 | return self._handler 19 | 20 | @handler.setter 21 | def handler(self, value): 22 | self._handler = value 23 | 24 | def __call__(self, environ, start_response): 25 | # Retrieve the CORS response details. 26 | request = webob.Request(environ) 27 | cors_response = self.handler.handle(request.method, request.headers) 28 | 29 | headers = cors_response.headers 30 | if cors_response.state == http_response.ResponseState.END: 31 | # Response should end immediately. Set the status and any headers 32 | # and exit. 33 | start_response(cors_response.status, headers.items()) 34 | return [] 35 | else: 36 | # Response should continue to the user's app. Set any CORS-specific 37 | # headers and keep going. 38 | response = request.get_response(self.app) 39 | for key, value in headers.items(): 40 | response.headers.add(key, value) 41 | return response(environ, start_response) 42 | -------------------------------------------------------------------------------- /cors/cors_handler.py: -------------------------------------------------------------------------------- 1 | """Processes a CORS request.""" 2 | 3 | from cors import constants 4 | from cors import filters 5 | from cors import http_response 6 | 7 | 8 | class CorsHandler(object): 9 | """Processes a CORS request and returns the CORS response.""" 10 | 11 | def __init__(self, options): 12 | self._filters = filters.Filters(options) 13 | 14 | def handle(self, http_method=None, headers=None): 15 | """Processes a CORS request and returns the CORS response. 16 | 17 | Returns an object with the HTTP headers to set on the response. 18 | """ 19 | request = CorsRequest(http_method, headers) 20 | response = CorsResponse() 21 | 22 | error = self._filters.run(request, response) 23 | 24 | return http_response.create(request, response, error) 25 | 26 | 27 | class CorsRequest(object): 28 | 29 | def __init__(self, http_method=None, headers=None): 30 | self.http_method = http_method 31 | self.origin = None 32 | self.request_method = None 33 | self.request_headers = None 34 | self.is_cors = False 35 | self.is_preflight = False 36 | 37 | self.headers = {} 38 | # Load CORS-specific headers. 39 | if headers: 40 | for key, value in headers.items(): 41 | self.headers[key] = value 42 | key = key.lower() 43 | if key == constants.ORIGIN.lower(): 44 | self.origin = value 45 | elif key == constants.ACCESS_CONTROL_REQUEST_METHOD.lower(): 46 | self.request_method = value 47 | elif key == constants.ACCESS_CONTROL_REQUEST_HEADERS.lower(): 48 | self.request_headers = [ 49 | x.strip() 50 | for x in value.split(',') 51 | ] 52 | 53 | # Detect whether the request is a CORS or preflight request. 54 | if self.origin: 55 | # If this request has an Origin, it is a CORS request. 56 | self.is_cors = True 57 | if (self.http_method == 'OPTIONS' and 58 | self.request_method is not None): 59 | # If this is an OPTIONS request with an 60 | # Access-Control-Request-Method header, its a preflight. 61 | self.is_preflight = True 62 | 63 | 64 | class CorsResponse(object): 65 | 66 | def __init__(self): 67 | self.allow_origin = None 68 | self.allow_credentials = False 69 | self.max_age = None 70 | self.expose_headers = [] 71 | self.allow_methods = None 72 | self.allow_headers = None 73 | self.headers = {} 74 | -------------------------------------------------------------------------------- /cors/cors_options.py: -------------------------------------------------------------------------------- 1 | """Options for configuring the CORS handler.""" 2 | 3 | import types 4 | 5 | from cors import validators 6 | 7 | 8 | ALL_ORIGINS = '*' 9 | DEFAULT_METHODS = ['HEAD', 'GET', 'PUT', 'POST', 'DELETE'] 10 | 11 | 12 | class CorsOptions(object): 13 | """Stores options for configuring the CORS handler.""" 14 | 15 | def __init__(self, 16 | allow_origins=True, 17 | allow_credentials=False, 18 | allow_methods=None, 19 | allow_headers=True, 20 | expose_headers=None, 21 | max_age=None, 22 | vary=None, 23 | allow_non_cors_requests=True, 24 | continue_on_error=False): 25 | """ 26 | allow_origins (Validator) - The origins that are allowed. Set to True 27 | to allow all origins, or to a list of valid origins. Defaults to 28 | True, which allows all origins, and appends the 29 | Access-Control-Allow-Origin: * response header. 30 | 31 | allow_credentials (bool) - Whether or not the app supports credentials. 32 | If True, appends the Access-Control-Allow-Credentials: true header. 33 | Defaults to False. 34 | 35 | allow_methods (Validator) - The HTTP methods that are allowed. Set to 36 | True to allow all methods, or to a list of allowed methods. Defauts to 37 | ['HEAD', 'GET', 'PUT', 'POST', 'DELETE'], which appends the 38 | Access-Control-Allow-Methods: HEAD, GET, PUT, POST, DELETE response 39 | header. 40 | 41 | allow_headers (Validator) - The HTTP request headers that are allowed. 42 | Set to True to allow all headers, or to a list of allowed headers. 43 | Defaults to True, which appends the Access-Control-Allow-Headers 44 | response header. 45 | 46 | expose_headers (list of strings) - List of response headers to expose 47 | to the client. Defaults to None. Appends the 48 | Access-Control-Expose-Headers response header. 49 | 50 | max_age (int) - The maximum time (in seconds) to cache the preflight 51 | response. Defaults to None, which doesn't append any response headers. 52 | Appends the Access-Control-Max-Age header when set. 53 | 54 | vary (bool) - Set to True if the Vary: Origin header should be appended 55 | to the response, False otherwise. The Vary header is useful when used 56 | in conjunction with a list of valid origins, and tells downstream 57 | proxy servers not to cache the response based on Origin. The default 58 | value is False for '*' origins, True otherwise. 59 | 60 | allow_non_cors_requests (bool) - Whether non-CORS requests should be 61 | allowed. Defaults to True. 62 | 63 | continue_on_error (bool) - Whether an invalid CORS request should 64 | trigger an error, or continue processing. Defaults to False. 65 | """ 66 | self.origin_validator = validators.create(allow_origins) 67 | 68 | if allow_methods is None: 69 | allow_methods = DEFAULT_METHODS 70 | self.methods_validator = validators.create(allow_methods) 71 | 72 | if allow_headers is None: 73 | allow_headers = [] 74 | self.headers_validator = validators.create(allow_headers) 75 | 76 | self.allow_credentials = allow_credentials 77 | 78 | if expose_headers is None: 79 | expose_headers = [] 80 | self.expose_headers = expose_headers 81 | 82 | if max_age and not isinstance(max_age, types.IntType): 83 | raise TypeError('max_age must be an int.') 84 | self.max_age = max_age 85 | 86 | # The *_value properties below are the actual values to use in the 87 | # Access-Control-Allow-* headers. Set to None if the value is based 88 | # on the request and cannot be precalculated. Otherwise these values 89 | # are set now. 90 | 91 | # Only set the origin value if it is '*', since that is the only option 92 | # that can be precalculated (The actual origin value depends on the 93 | # request). 94 | self.origin_value = None 95 | if allow_origins is True: 96 | self.origin_value = ALL_ORIGINS 97 | 98 | # Only set the methods and headers if they are a list. If they are a 99 | # list, the entire list is returned in the preflight response. If they 100 | # are not a list (bool, regex, funciton, etc), then the request values 101 | # are echoed back to the user (and the values below are set to None 102 | # since they can't be precalculated). 103 | self.methods_value = None 104 | if isinstance(allow_methods, types.ListType): 105 | self.methods_value = allow_methods 106 | 107 | self.headers_value = None 108 | if isinstance(allow_headers, types.ListType): 109 | self.headers_value = allow_headers 110 | 111 | if vary is None: 112 | vary = True 113 | self.vary = vary 114 | 115 | self.allow_non_cors_requests = allow_non_cors_requests 116 | self.continue_on_error = continue_on_error 117 | -------------------------------------------------------------------------------- /cors/errors.py: -------------------------------------------------------------------------------- 1 | class CorsError(Exception): 2 | """Base exception for all CORS-related errors.""" 3 | 4 | def __init__(self, status='500 Internal Server Error'): 5 | Exception.__init__(self, status) 6 | self.status = status 7 | 8 | 9 | class OriginError(CorsError): 10 | """Invalid request origin.""" 11 | 12 | def __init__(self, origin): 13 | CorsError.__init__(self, '403 Forbidden') 14 | self.origin = origin 15 | 16 | def __str__(self): 17 | return 'Disallowed origin: %s' % self.origin 18 | 19 | 20 | class MethodError(CorsError): 21 | """Invalid request method.""" 22 | 23 | def __init__(self, request_method): 24 | CorsError.__init__(self, '405 Method Not Allowed') 25 | self.request_method = request_method 26 | 27 | def __str__(self): 28 | return 'HTTP method %s not allowed.' % self.request_method 29 | 30 | 31 | class HeadersError(CorsError): 32 | """Invalid request header(s).""" 33 | 34 | def __init__(self, invalid_headers): 35 | CorsError.__init__(self, '403 Forbidden') 36 | self.invalid_headers = invalid_headers 37 | 38 | def __str__(self): 39 | return 'Headers not allowed: %s' % ','.join(self.invalid_headers) 40 | 41 | 42 | class NonCorsRequestError(CorsError): 43 | """Non-cors request (if non-cors requests are disabled).""" 44 | 45 | def __init__(self): 46 | CorsError.__init__(self, '400 Bad Request') 47 | 48 | def __str__(self): 49 | return 'Non-CORS requests not allowed' 50 | -------------------------------------------------------------------------------- /cors/filters.py: -------------------------------------------------------------------------------- 1 | """Defines the filters for processing CORS requests.""" 2 | 3 | import errors 4 | import logging 5 | 6 | from cors import constants 7 | 8 | 9 | class Filters(object): 10 | 11 | def __init__(self, options): 12 | all_filters = { 13 | 'allow_credentials': AllowCredentialsFilter(options), 14 | 'allow_headers': AllowHeadersFilter(options), 15 | 'allow_methods': AllowMethodsFilter(options), 16 | 'allow_non_cors_request': AllowNonCorsRequestFilter(options), 17 | 'allow_origin': AllowOriginFilter(options), 18 | 'expose_headers': ExposeHeadersFilter(options), 19 | 'max_age': MaxAgeFilter(options), 20 | 'vary': VaryFilter(options) 21 | } 22 | 23 | self.cors_filters = self.create_filters( 24 | all_filters, 25 | 'vary', 26 | 'allow_origin', 27 | 'allow_credentials', 28 | 'expose_headers' 29 | ) 30 | 31 | self.preflight_filters = self.create_filters( 32 | all_filters, 33 | 'vary', 34 | 'allow_origin', 35 | 'allow_methods', 36 | 'allow_headers', 37 | 'allow_credentials', 38 | 'max_age' 39 | ) 40 | 41 | self.non_cors_filters = self.create_filters( 42 | all_filters, 43 | 'vary', 44 | 'allow_non_cors_request' 45 | ) 46 | 47 | self.continue_on_error = options.continue_on_error 48 | 49 | def create_filters(self, all_filters, *args): 50 | filters = [] 51 | for arg in args: 52 | filters.append(all_filters[arg]) 53 | return filters 54 | 55 | def run(self, request, response): 56 | filters = self.choose_filters(request) 57 | for f in filters: 58 | error = f.filter(request, response) 59 | if error: 60 | logging.error(error) 61 | if not self.continue_on_error: 62 | return error 63 | return None 64 | 65 | def choose_filters(self, request): 66 | if request.is_preflight: 67 | return self.preflight_filters 68 | elif request.is_cors: 69 | return self.cors_filters 70 | else: 71 | return self.non_cors_filters 72 | 73 | 74 | class Filter(object): 75 | """ 76 | The CORS request is processed through a series of filters. 77 | 78 | Each filter is responsible for a single activity (for example, validating 79 | the Origin header). Filters derive from this Filter class, which implements 80 | a single filter() method. The filter() method takes in a request and 81 | response object. 82 | 83 | The request stores all the CORS-related information from the request, while 84 | the response object stores any CORS-related information to set on the HTTP 85 | response. If there is an error processing the response, the filter() method 86 | returns a CorsException (note the exception is not thrown, it is returned). 87 | If there are no errors, the filter() method returns None. 88 | """ 89 | 90 | def __init__(self, options): 91 | self.options = options 92 | 93 | 94 | class AllowCredentialsFilter(Filter): 95 | 96 | def __init__(self, options): 97 | Filter.__init__(self, options) 98 | 99 | def filter(self, request, response): 100 | if self.options.allow_credentials: 101 | response.allow_credentials = True 102 | 103 | 104 | class AllowHeadersFilter(Filter): 105 | 106 | def __init__(self, options): 107 | Filter.__init__(self, options) 108 | 109 | def filter(self, request, response): 110 | request_headers = request.request_headers 111 | if not request_headers: 112 | return 113 | if not len(request_headers): 114 | return 115 | 116 | valid = [] 117 | not_valid = [] 118 | for header in request_headers: 119 | if self.options.headers_validator.is_valid(header): 120 | valid.append(header) 121 | else: 122 | not_valid.append(header) 123 | 124 | headers_value = self.options.headers_value 125 | 126 | if len(not_valid): 127 | response.allow_headers = headers_value 128 | return errors.HeadersError(not_valid) 129 | 130 | if not headers_value: 131 | headers_value = valid 132 | response.allow_headers = headers_value 133 | 134 | 135 | class AllowMethodsFilter(Filter): 136 | 137 | def __init__(self, options): 138 | Filter.__init__(self, options) 139 | 140 | def filter(self, request, response): 141 | is_valid = self.options.methods_validator.is_valid( 142 | request.request_method) 143 | 144 | allow_methods = self.options.methods_value 145 | 146 | if not is_valid: 147 | response.allow_methods = allow_methods 148 | return errors.MethodError(request.request_method) 149 | 150 | if not allow_methods: 151 | allow_methods = [request.request_method] 152 | response.allow_methods = allow_methods 153 | 154 | 155 | class AllowNonCorsRequestFilter(Filter): 156 | 157 | def __init__(self, options): 158 | Filter.__init__(self, options) 159 | 160 | def filter(self, request, response): 161 | if not self.options.allow_non_cors_requests: 162 | return errors.NonCorsRequestError() 163 | 164 | 165 | class AllowOriginFilter(Filter): 166 | 167 | def __init__(self, options): 168 | Filter.__init__(self, options) 169 | 170 | def filter(self, request, response): 171 | origin = request.origin 172 | is_valid_origin = self.options.origin_validator.is_valid(origin) 173 | 174 | origin_value = self.options.origin_value 175 | 176 | if not is_valid_origin: 177 | response.allow_origin = origin_value 178 | return errors.OriginError(origin) 179 | 180 | if self.options.allow_credentials or not origin_value: 181 | origin_value = origin 182 | 183 | response.allow_origin = origin_value 184 | 185 | 186 | class ExposeHeadersFilter(Filter): 187 | 188 | def __init__(self, options): 189 | Filter.__init__(self, options) 190 | 191 | def filter(self, request, response): 192 | if len(self.options.expose_headers): 193 | response.expose_headers = self.options.expose_headers 194 | 195 | 196 | class MaxAgeFilter(Filter): 197 | 198 | def __init__(self, options): 199 | Filter.__init__(self, options) 200 | 201 | def filter(self, request, response): 202 | if self.options.max_age: 203 | response.max_age = self.options.max_age 204 | 205 | 206 | class VaryFilter(Filter): 207 | 208 | def __init__(self, options): 209 | Filter.__init__(self, options) 210 | 211 | def filter(self, request, response): 212 | if self.options.vary: 213 | response.headers[constants.VARY] = constants.ORIGIN 214 | -------------------------------------------------------------------------------- /cors/http_response.py: -------------------------------------------------------------------------------- 1 | import httplib 2 | 3 | from cors import constants 4 | 5 | 6 | class ResponseState(object): 7 | """Indicates how the response should behave.""" 8 | 9 | END = 1 # The response should end. Do not pass go. Do not collect $200. 10 | CONTINUE = 2 # The response should continue to the downstream handlers. 11 | 12 | 13 | class HttpResponse(object): 14 | """Stores the CORS-specific response details. 15 | 16 | This class contains any response headers that should be set on the response 17 | as well as a state property, which indicates whether the request should 18 | continue or end. If the response ends, the status property indicates the 19 | recommended HTTP status code for the response. 20 | """ 21 | def __init__(self): 22 | self.headers = {} 23 | self.state = ResponseState.CONTINUE 24 | # '200 OK' 25 | self.status = '%s %s' % (httplib.OK, httplib.responses[httplib.OK]) 26 | self.error = None 27 | 28 | def end(self, error=None): 29 | if error: 30 | self.status = error.status 31 | self.error = error 32 | self.state = ResponseState.END 33 | 34 | 35 | def create(request, response, error=None): 36 | """Creates a new HttpResponse instance. 37 | 38 | Args: 39 | request (CorsRequest) - The request details. 40 | response (CorsResponse) - The response details. 41 | error (CorsException) - The error (if any). Defaults to None. 42 | """ 43 | http_response = HttpResponse() 44 | 45 | # Set any generic response headers (such as Vary). 46 | for key, value in response.headers.items(): 47 | http_response.headers[key] = value 48 | 49 | if error: 50 | # If there is an error, return immediately, do not set any 51 | # CORS-specific headers. 52 | http_response.end(error) 53 | return http_response 54 | 55 | # The Access-Control-Allow-Origin and Access-Control-Allow-Credentials are 56 | # set on both CORS and preflight responses. 57 | if response.allow_origin: 58 | http_response.headers[constants.ACCESS_CONTROL_ALLOW_ORIGIN] = \ 59 | response.allow_origin 60 | if response.allow_credentials: 61 | http_response.headers[constants.ACCESS_CONTROL_ALLOW_CREDENTIALS] = \ 62 | 'true' 63 | 64 | if request.is_preflight: 65 | # Set the preflight-only headers. 66 | if len(response.allow_methods): 67 | http_response.headers[constants.ACCESS_CONTROL_ALLOW_METHODS] = \ 68 | ','.join(response.allow_methods) 69 | if response.allow_headers and len(response.allow_headers): 70 | http_response.headers[constants.ACCESS_CONTROL_ALLOW_HEADERS] = \ 71 | ','.join(response.allow_headers) 72 | if response.max_age: 73 | http_response.headers[constants.ACCESS_CONTROL_MAX_AGE] = \ 74 | str(response.max_age) 75 | # '200 OK' 76 | http_response.status = '%s %s' % ( 77 | httplib.OK, 78 | httplib.responses[httplib.OK] 79 | ) 80 | http_response.end() 81 | else: 82 | # Set the CORS-only headers. 83 | if len(response.expose_headers): 84 | http_response.headers[constants.ACCESS_CONTROL_EXPOSE_HEADERS] = \ 85 | ','.join(response.expose_headers) 86 | return http_response 87 | -------------------------------------------------------------------------------- /cors/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | import types 3 | 4 | 5 | def create(obj): 6 | obj_type = type(obj) 7 | if isinstance(obj, types.BooleanType): 8 | return BooleanValidator(obj) 9 | if isinstance(obj, types.ListType): 10 | return ListValidator(obj) 11 | if isinstance(obj, re._pattern_type): 12 | return RegexValidator(obj) 13 | 14 | raise Exception('Validator not found for type: %s' % str(obj_type)) 15 | 16 | 17 | class Validator(object): 18 | 19 | def __init__(self): 20 | pass 21 | 22 | def is_valid(self, value): 23 | pass 24 | 25 | 26 | class BooleanValidator(Validator): 27 | 28 | def __init__(self, value): 29 | Validator.__init__(self) 30 | self.value = value 31 | 32 | def is_valid(self, value): 33 | return self.value 34 | 35 | 36 | class ListValidator(Validator): 37 | 38 | def __init__(self, values=None): 39 | Validator.__init__(self) 40 | if values is None: 41 | values = [] 42 | self.values = values 43 | 44 | def is_valid(self, value): 45 | for item in self.values: 46 | if item.lower() == value.lower(): 47 | return True 48 | return False 49 | 50 | 51 | class RegexValidator(Validator): 52 | 53 | def __init__(self, value): 54 | Validator.__init__(self) 55 | self.value = value 56 | 57 | def is_valid(self, value): 58 | return self.value.match(value) is not None 59 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsur/cors-python/9eb64ae395f0685ba957e267a4512eeb44752830/examples/__init__.py -------------------------------------------------------------------------------- /examples/app-engine/.gitignore: -------------------------------------------------------------------------------- 1 | cors 2 | -------------------------------------------------------------------------------- /examples/app-engine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsur/cors-python/9eb64ae395f0685ba957e267a4512eeb44752830/examples/app-engine/__init__.py -------------------------------------------------------------------------------- /examples/app-engine/app.yaml: -------------------------------------------------------------------------------- 1 | application: app-engine 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | 7 | handlers: 8 | - url: /favicon\.ico 9 | static_files: favicon.ico 10 | upload: favicon\.ico 11 | 12 | - url: .* 13 | script: main.app 14 | 15 | libraries: 16 | - name: webapp2 17 | version: "2.5.2" 18 | -------------------------------------------------------------------------------- /examples/app-engine/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import webapp2 18 | from cors.cors_application import CorsApplication 19 | from cors.cors_options import CorsOptions 20 | 21 | 22 | class MainHandler(webapp2.RequestHandler): 23 | def get(self): 24 | self.response.write('Hello world!') 25 | 26 | webapp = webapp2.WSGIApplication([ 27 | ('/', MainHandler) 28 | ], debug=True) 29 | app = CorsApplication(webapp, CorsOptions(allow_origins=['http://blah.com'], 30 | allow_headers=['X-Foo'], 31 | continue_on_error=True)) 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | setup( 5 | name='cors-python', 6 | version='0.0.1', 7 | description='Python CORS Integration for WSGI Applications', 8 | author='Monsur Hossain', 9 | author_email='monsur@gmail.com', 10 | url='https://github.com/monsur/cors-python', 11 | packages=find_packages(exclude=('tests',)), 12 | requires=['WebOb'] 13 | ) 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monsur/cors-python/9eb64ae395f0685ba957e267a4512eeb44752830/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cors_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from cors import constants 3 | from cors import cors_handler 4 | 5 | 6 | class TestCorsRequest(unittest.TestCase): 7 | 8 | def test_defaultInstance(self): 9 | req = cors_handler.CorsRequest() 10 | self.assertIsNone(req.http_method) 11 | self.assertIsNone(req.origin) 12 | self.assertIsNone(req.request_method) 13 | self.assertIsNone(req.request_headers) 14 | self.assertFalse(req.is_cors) 15 | self.assertFalse(req.is_preflight) 16 | 17 | def test_setMethod(self): 18 | req = cors_handler.CorsRequest('GET') 19 | self.assertEquals('GET', req.http_method) 20 | 21 | def test_headers(self): 22 | headers = {} 23 | headers[constants.ORIGIN] = 'http://github.com' 24 | headers[constants.ACCESS_CONTROL_REQUEST_METHOD] = 'GET' 25 | headers[constants.ACCESS_CONTROL_REQUEST_HEADERS] = 'Header1, Header2' 26 | req = cors_handler.CorsRequest('GET', headers) 27 | 28 | self.assertEquals('http://github.com', req.origin) 29 | self.assertEquals('GET', req.request_method) 30 | self.assertEquals(['Header1', 'Header2'], req.request_headers) 31 | 32 | def test_isCors(self): 33 | headers = {} 34 | headers[constants.ORIGIN] = 'http://github.com' 35 | req = cors_handler.CorsRequest('GET', headers) 36 | self.assertTrue(req.is_cors) 37 | self.assertFalse(req.is_preflight) 38 | 39 | def test_isPreflight(self): 40 | headers = {} 41 | headers[constants.ORIGIN] = 'http://github.com' 42 | headers[constants.ACCESS_CONTROL_REQUEST_METHOD] = 'PUT' 43 | req = cors_handler.CorsRequest('OPTIONS', headers) 44 | self.assertTrue(req.is_cors) 45 | self.assertTrue(req.is_preflight) 46 | 47 | def test_notPreflight1(self): 48 | headers = {} 49 | headers[constants.ORIGIN] = 'http://github.com' 50 | headers[constants.ACCESS_CONTROL_REQUEST_METHOD] = 'PUT' 51 | req = cors_handler.CorsRequest('GET', headers) 52 | self.assertTrue(req.is_cors) 53 | self.assertFalse(req.is_preflight) 54 | 55 | def test_notPreflight2(self): 56 | headers = {} 57 | headers['Origin'] = 'http://github.com' 58 | req = cors_handler.CorsRequest('OPTIONS', headers) 59 | self.assertTrue(req.is_cors) 60 | self.assertFalse(req.is_preflight) 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /tests/test_cors_options.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cors import validators 4 | from cors import cors_options 5 | 6 | 7 | class TestCorsOptions(unittest.TestCase): 8 | 9 | def test_defaultInstance(self): 10 | o = cors_options.CorsOptions() 11 | self.assertTrue( 12 | isinstance(o.origin_validator, validators.BooleanValidator) 13 | ) 14 | self.assertEquals(cors_options.ALL_ORIGINS, o.origin_value) 15 | self.assertTrue( 16 | isinstance(o.methods_validator, validators.ListValidator) 17 | ) 18 | self.assertEquals(cors_options.DEFAULT_METHODS, o.methods_value) 19 | self.assertTrue( 20 | isinstance(o.headers_validator, validators.BooleanValidator) 21 | ) 22 | self.assertIsNone(o.headers_value) 23 | self.assertEquals([], o.expose_headers) 24 | self.assertFalse(o.allow_credentials) 25 | self.assertTrue(o.vary) 26 | self.assertIsNone(o.max_age) 27 | 28 | def test_originsList(self): 29 | o = cors_options.CorsOptions(allow_origins=['http://foo.com']) 30 | self.assertTrue( 31 | isinstance(o.origin_validator, validators.ListValidator) 32 | ) 33 | self.assertIsNone(o.origin_value) 34 | self.assertTrue(o.vary) 35 | 36 | def test_allowCredentials(self): 37 | o = cors_options.CorsOptions(allow_credentials=True) 38 | self.assertTrue(o.allow_credentials) 39 | 40 | def test_exposeHeaders(self): 41 | o = cors_options.CorsOptions(expose_headers=['Header1']) 42 | self.assertEquals(['Header1'], o.expose_headers) 43 | 44 | def test_maxAge(self): 45 | o = cors_options.CorsOptions(max_age=1200) 46 | self.assertEquals(1200, o.max_age) 47 | 48 | def test_invalidMaxAge(self): 49 | try: 50 | _ = cors_options.CorsOptions(max_age='foo') 51 | except: 52 | return 53 | self.fail('Expected TypeError') 54 | 55 | def test_allowMethods(self): 56 | o = cors_options.CorsOptions(allow_methods=['foo']) 57 | self.assertTrue( 58 | isinstance(o.methods_validator, validators.ListValidator) 59 | ) 60 | self.assertEquals(['foo'], o.methods_value) 61 | 62 | def test_allowAllMethods(self): 63 | o = cors_options.CorsOptions(allow_methods=True) 64 | self.assertTrue( 65 | isinstance(o.methods_validator, validators.BooleanValidator) 66 | ) 67 | self.assertIsNone(o.methods_value) 68 | 69 | def test_allowHeaders(self): 70 | o = cors_options.CorsOptions(allow_headers=['foo']) 71 | self.assertTrue( 72 | isinstance(o.headers_validator, validators.ListValidator) 73 | ) 74 | self.assertEquals(['foo'], o.headers_value) 75 | 76 | def test_allowAllHeaders(self): 77 | o = cors_options.CorsOptions(allow_headers=True) 78 | self.assertTrue( 79 | isinstance(o.headers_validator, validators.BooleanValidator) 80 | ) 81 | self.assertIsNone(o.headers_value) 82 | 83 | 84 | if __name__ == '__main__': 85 | unittest.main() 86 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from cors import constants 3 | from cors import errors 4 | from cors import filters 5 | from cors import cors_options 6 | from cors import cors_handler 7 | 8 | 9 | class TestAllowCredentialsFilter(unittest.TestCase): 10 | 11 | def test_addHeader(self): 12 | options = cors_options.CorsOptions(allow_credentials=True) 13 | response = cors_handler.CorsResponse() 14 | f = filters.AllowCredentialsFilter(options) 15 | f.filter(None, response) 16 | self.assertTrue(response.allow_credentials) 17 | 18 | def test_noHeader(self): 19 | options = cors_options.CorsOptions(allow_credentials=False) 20 | response = cors_handler.CorsResponse() 21 | f = filters.AllowCredentialsFilter(options) 22 | f.filter(None, response) 23 | self.assertFalse(response.allow_credentials) 24 | 25 | 26 | class TestAllowNonCorsRequestFilter(unittest.TestCase): 27 | 28 | def test_allow(self): 29 | options = cors_options.CorsOptions(allow_non_cors_requests=True) 30 | f = filters.AllowNonCorsRequestFilter(options) 31 | error = f.filter(None, None) 32 | self.assertIsNone(error) 33 | 34 | def test_disallow(self): 35 | options = cors_options.CorsOptions(allow_non_cors_requests=False) 36 | f = filters.AllowNonCorsRequestFilter(options) 37 | error = f.filter(None, None) 38 | self.assertIsNotNone(error) 39 | 40 | 41 | class TestAllowOriginFilter(unittest.TestCase): 42 | 43 | def test_allowAllOrigins(self): 44 | options = cors_options.CorsOptions() 45 | f = filters.AllowOriginFilter(options) 46 | 47 | # Test with no Origin 48 | request = cors_handler.CorsRequest() 49 | response = cors_handler.CorsResponse() 50 | error = f.filter(request, response) 51 | self.assertIsNone(error) 52 | self.assertEquals('*', response.allow_origin) 53 | 54 | # Test CORS request 55 | request = cors_handler.CorsRequest( 56 | 'GET', 57 | { 58 | constants.ORIGIN: 'http://foo.com' 59 | } 60 | ) 61 | response = cors_handler.CorsResponse() 62 | error = f.filter(request, response) 63 | self.assertIsNone(error) 64 | self.assertEquals('*', response.allow_origin) 65 | 66 | # Test CORS preflight request 67 | request = cors_handler.CorsRequest( 68 | 'OPTIONS', { 69 | constants.ORIGIN: 'http://foo.com', 70 | constants.ACCESS_CONTROL_REQUEST_METHOD: 'GET' 71 | } 72 | ) 73 | response = cors_handler.CorsResponse() 74 | error = f.filter(request, response) 75 | self.assertIsNone(error) 76 | self.assertEquals('*', response.allow_origin) 77 | 78 | def test_invalidOrigin(self): 79 | options = cors_options.CorsOptions(allow_origins=['http://foo.com']) 80 | f = filters.AllowOriginFilter(options) 81 | request = cors_handler.CorsRequest( 82 | 'GET', 83 | { 84 | constants.ORIGIN: 'http://bar.com' 85 | } 86 | ) 87 | response = cors_handler.CorsResponse() 88 | error = f.filter(request, response) 89 | self.assertIsInstance(error, errors.OriginError) 90 | self.assertIsNone(response.allow_origin) 91 | 92 | def test_validOrigin(self): 93 | options = cors_options.CorsOptions(allow_origins=['http://foo.com']) 94 | f = filters.AllowOriginFilter(options) 95 | request = cors_handler.CorsRequest( 96 | 'GET', 97 | { 98 | constants.ORIGIN: 'http://foo.com' 99 | } 100 | ) 101 | response = cors_handler.CorsResponse() 102 | error = f.filter(request, response) 103 | self.assertIsNone(error) 104 | self.assertEquals('http://foo.com', response.allow_origin) 105 | 106 | def test_allowCredentials(self): 107 | options = cors_options.CorsOptions( 108 | allow_origins=True, 109 | allow_credentials=True 110 | ) 111 | f = filters.AllowOriginFilter(options) 112 | request = cors_handler.CorsRequest( 113 | 'GET', 114 | { 115 | constants.ORIGIN: 'http://foo.com' 116 | } 117 | ) 118 | response = cors_handler.CorsResponse() 119 | error = f.filter(request, response) 120 | self.assertIsNone(error) 121 | self.assertEquals('http://foo.com', response.allow_origin) 122 | 123 | 124 | class TestExposeHeadersFilter(unittest.TestCase): 125 | 126 | def test_noHeader(self): 127 | options = cors_options.CorsOptions() 128 | response = cors_handler.CorsResponse() 129 | f = filters.ExposeHeadersFilter(options) 130 | f.filter(None, response) 131 | self.assertEquals(0, len(response.expose_headers)) 132 | 133 | def test_addHeader(self): 134 | options = cors_options.CorsOptions(expose_headers=["Foo"]) 135 | response = cors_handler.CorsResponse() 136 | f = filters.ExposeHeadersFilter(options) 137 | f.filter(None, response) 138 | self.assertEquals(1, len(response.expose_headers)) 139 | self.assertEquals('Foo', response.expose_headers[0]) 140 | 141 | 142 | class TestMaxAgeFilter(unittest.TestCase): 143 | 144 | def test_noHeader(self): 145 | options = cors_options.CorsOptions() 146 | response = cors_handler.CorsResponse() 147 | f = filters.MaxAgeFilter(options) 148 | f.filter(None, response) 149 | self.assertIsNone(response.max_age) 150 | 151 | def test_addHeader(self): 152 | options = cors_options.CorsOptions(max_age=1000) 153 | response = cors_handler.CorsResponse() 154 | f = filters.MaxAgeFilter(options) 155 | f.filter(None, response) 156 | self.assertEquals(1000, response.max_age) 157 | 158 | 159 | class TestVaryFilter(unittest.TestCase): 160 | 161 | def test_addHeader(self): 162 | options = cors_options.CorsOptions(vary=True) 163 | response = cors_handler.CorsResponse() 164 | filtr = filters.VaryFilter(options) 165 | filtr.filter(None, response) 166 | self.assertEquals(constants.ORIGIN, response.headers[constants.VARY]) 167 | 168 | def test_noHeader(self): 169 | options = cors_options.CorsOptions(vary=False) 170 | response = cors_handler.CorsResponse() 171 | filtr = filters.VaryFilter(options) 172 | filtr.filter(None, response) 173 | self.assertFalse(constants.VARY in response.headers) 174 | 175 | 176 | if __name__ == '__main__': 177 | unittest.main() 178 | -------------------------------------------------------------------------------- /tests/test_validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | 4 | from cors import validators 5 | 6 | 7 | class TestValidators(unittest.TestCase): 8 | 9 | def test_create(self): 10 | self.subtest_create(validators.BooleanValidator, True) 11 | self.subtest_create(validators.BooleanValidator, False) 12 | self.subtest_create(validators.ListValidator, []) 13 | 14 | try: 15 | validators.create({}) 16 | self.fail('Expected exception') 17 | except Exception: 18 | return 19 | 20 | def subtest_create(self, t, v): 21 | self.assertTrue(isinstance(validators.create(v), t)) 22 | 23 | 24 | class TestBooleanValidator(unittest.TestCase): 25 | 26 | def test_true(self): 27 | true_validator = validators.BooleanValidator(True) 28 | self.assertTrue(true_validator.is_valid('random value')) 29 | 30 | def test_false(self): 31 | false_validator = validators.BooleanValidator(False) 32 | self.assertFalse(false_validator.is_valid('random value')) 33 | 34 | 35 | class TestListValidator(unittest.TestCase): 36 | 37 | def test_emptyList(self): 38 | v = validators.ListValidator() 39 | self.assertEquals([], v.values) 40 | self.assertFalse(v.is_valid('foo')) 41 | 42 | def test_valid(self): 43 | v = validators.ListValidator(['a', 'b', 'c']) 44 | self.assertTrue(v.is_valid('a')) 45 | self.assertFalse(v.is_valid('foo')) 46 | 47 | def test_validCaseSensitive(self): 48 | v = validators.ListValidator(['A', 'b', 'C']) 49 | self.assertTrue(v.is_valid('a')) 50 | self.assertTrue(v.is_valid('B')) 51 | 52 | 53 | class TestRegexValidator(unittest.TestCase): 54 | 55 | def test_valid(self): 56 | pattern = re.compile("foo") 57 | v = validators.RegexValidator(pattern) 58 | self.assertTrue(v.is_valid('foo')) 59 | self.assertFalse(v.is_valid('bar')) 60 | 61 | 62 | if __name__ == '__main__': 63 | unittest.main() 64 | --------------------------------------------------------------------------------