From edbefbcf4fb6ca7bc0e724dea8c20dc87df3c127 Mon Sep 17 00:00:00 2001 From: Max Goodman Date: Wed, 28 Sep 2011 22:42:13 -0700 Subject: [PATCH] Add CORS support and cross-domain response helpers. --- r2/r2/controllers/reddit_base.py | 55 ++++++++++++++++++++++++++++++++ r2/r2/lib/app_globals.py | 1 + r2/r2/lib/base.py | 10 ++++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index 8f1e9b483..d97aea530 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -56,6 +56,7 @@ cache_affecting_cookies = ('reddit_first','over18','_options') class Cookies(dict): def add(self, name, value, *k, **kw): + name = name.encode('utf-8') self[name] = Cookie(value, *k, **kw) class Cookie(object): @@ -467,6 +468,32 @@ def paginated_listing(default_page_size=25, max_page_size=100): def base_listing(fn): return paginated_listing()(fn) +def cross_domain(origins, **options): + """Set up cross domain validation and hoisting for a request handler.""" + origins = filter(None, origins) + def cross_domain_wrap(fn): + def cross_domain_handler(self, *args, **kwargs): + if request.params.get("hoist") == "cookie": + # Cookie polling response + if g.origin in origins: + name = request.environ["pylons.routes_dict"]["action_name"] + resp = fn(self, *args, **kwargs) + c.cookies.add('hoist_%s' % name, ''.join(resp.content)) + c.response_content_type = 'text/html' + resp.content = '' + return resp + else: + abort(403) + else: + self.check_cors() + return fn(self, *args, **kwargs) + + cross_domain_handler.cors_perms = { + "allowed_origins": origins, + "allow_credentials": bool(options.get("allow_credentials")) + } + return cross_domain_handler + return cross_domain_wrap class MinimalController(BaseController): @@ -616,6 +643,34 @@ class MinimalController(BaseController): def abort403(self): abort(403, "forbidden") + def check_cors(self): + origin = request.headers.get("Origin") + if not origin: + return + + method = request.method + if method == 'OPTIONS': + # preflight request + method = request.headers.get("Access-Control-Request-Method") + if not method: + self.abort403() + + action = request.environ["pylons.routes_dict"]["action_name"] + + handler = getattr(self, method + "_" + action, None) + cors = handler and getattr(handler, "cors_perms", None) + + if cors and origin in cors["allowed_origins"]: + response.headers["Access-Control-Allow-Origin"] = origin + if cors.get("allow_credentials"): + response.headers["Access-Control-Allow-Credentials"] = "true" + else: + self.abort403() + + def OPTIONS(self): + """Return empty responses for CORS preflight requests""" + self.check_cors() + def sendpng(self, string): c.response_content_type = 'image/png' c.response.content = string diff --git a/r2/r2/lib/app_globals.py b/r2/r2/lib/app_globals.py index 73b1b0402..0105ef3be 100755 --- a/r2/r2/lib/app_globals.py +++ b/r2/r2/lib/app_globals.py @@ -284,6 +284,7 @@ class Globals(object): self.REDDIT_MAIN = bool(os.environ.get('REDDIT_MAIN')) + self.origin = "http://" + self.domain self.secure_domains = set([urlparse(self.payment_domain).netloc]) # load the unique hashed names of files under static diff --git a/r2/r2/lib/base.py b/r2/r2/lib/base.py index 5166bd3a3..a9246cbe6 100644 --- a/r2/r2/lib/base.py +++ b/r2/r2/lib/base.py @@ -93,9 +93,15 @@ class BaseController(WSGIController): meth = request.method.upper() if meth == 'HEAD': meth = 'GET' - request.environ['pylons.routes_dict']['action'] = \ - meth + '_' + action + if meth != 'OPTIONS': + handler_name = meth + '_' + action + else: + handler_name = meth + + request.environ['pylons.routes_dict']['action_name'] = action + request.environ['pylons.routes_dict']['action'] = handler_name + c.response = Response() try: res = WSGIController.__call__(self, environ, start_response)