Commit 645f9c2c by Will Daly

Add proxy to allow IE9 to make xdomain requests

Adds an /xdomain_proxy.html endpoint that serves
the proxy file from the xdomain library.  This
allows IE9 users to iframe in the proxy page
to simulate a cross-domain request with cookies.
parent 61f7373a
"""Manage cross-domain configuration. """
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from cors_csrf.models import XDomainProxyConfiguration
admin.site.register(XDomainProxyConfiguration, ConfigurationModelAdmin)
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'XDomainProxyConfiguration'
db.create_table('cors_csrf_xdomainproxyconfiguration', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
('whitelist', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('cors_csrf', ['XDomainProxyConfiguration'])
def backwards(self, orm):
# Deleting model 'XDomainProxyConfiguration'
db.delete_table('cors_csrf_xdomainproxyconfiguration')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'cors_csrf.xdomainproxyconfiguration': {
'Meta': {'object_name': 'XDomainProxyConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'whitelist': ('django.db.models.fields.TextField', [], {})
}
}
complete_apps = ['cors_csrf']
"""Models for cross-domain configuration. """
from django.db import models
from django.utils.translation import ugettext_lazy as _
from config_models.models import ConfigurationModel
class XDomainProxyConfiguration(ConfigurationModel):
"""Cross-domain proxy configuration.
See `cors_csrf.views.xdomain_proxy` for an explanation of how this works.
"""
whitelist = models.fields.TextField(
help_text=_(
u"List of domains that are allowed to make cross-domain "
u"requests to this site. Please list each domain on its own line."
)
)
"""Tests for cross-domain request views. """
import json
from django.test import TestCase
from django.core.urlresolvers import reverse, NoReverseMatch
import ddt
from config_models.models import cache
from cors_csrf.models import XDomainProxyConfiguration
@ddt.ddt
class XDomainProxyTest(TestCase):
"""Tests for the xdomain proxy end-point. """
def setUp(self):
"""Clear model-based config cache. """
super(XDomainProxyTest, self).setUp()
try:
self.url = reverse('xdomain_proxy')
except NoReverseMatch:
self.skipTest('xdomain_proxy URL is not configured')
cache.clear()
def test_xdomain_proxy_disabled(self):
self._configure(False)
response = self._load_page()
self.assertEqual(response.status_code, 404)
@ddt.data(None, [' '], [' ', ' '])
def test_xdomain_proxy_enabled_no_whitelist(self, whitelist):
self._configure(True, whitelist=whitelist)
response = self._load_page()
self.assertEqual(response.status_code, 404)
@ddt.data(
(['example.com'], ['example.com']),
(['example.com', 'sub.example.com'], ['example.com', 'sub.example.com']),
([' example.com '], ['example.com']),
([' ', 'example.com'], ['example.com']),
)
@ddt.unpack
def test_xdomain_proxy_enabled_with_whitelist(self, whitelist, expected_whitelist):
self._configure(True, whitelist=whitelist)
response = self._load_page()
self._check_whitelist(response, expected_whitelist)
def _configure(self, is_enabled, whitelist=None):
"""Enable or disable the end-point and configure the whitelist. """
config = XDomainProxyConfiguration.current()
config.enabled = is_enabled
if whitelist:
config.whitelist = "\n".join(whitelist)
config.save()
cache.clear()
def _load_page(self):
"""Load the end-point. """
return self.client.get(reverse('xdomain_proxy'))
def _check_whitelist(self, response, expected_whitelist):
"""Verify that the domain whitelist is rendered on the page. """
rendered_whitelist = json.dumps({
domain: '*'
for domain in expected_whitelist
})
self.assertContains(response, 'xdomain.min.js')
self.assertContains(response, rendered_whitelist)
"""Views for enabling cross-domain requests. """
import logging
import json
from django.conf import settings
from django.views.decorators.cache import cache_page
from django.http import HttpResponseNotFound
from edxmako.shortcuts import render_to_response
from cors_csrf.models import XDomainProxyConfiguration
log = logging.getLogger(__name__)
XDOMAIN_PROXY_CACHE_TIMEOUT = getattr(settings, 'XDOMAIN_PROXY_CACHE_TIMEOUT', 60 * 15)
@cache_page(XDOMAIN_PROXY_CACHE_TIMEOUT)
def xdomain_proxy(request): # pylint: disable=unused-argument
"""Serve the xdomain proxy page.
Internet Explorer 9 does not send cookie information with CORS,
which means we can't make cross-domain POST requests that
require authentication (for example, from the course details
page on the marketing site to the enrollment API
to auto-enroll a user in an "honor" track).
The XDomain library [https://github.com/jpillora/xdomain]
provides an alternative to using CORS.
The library works as follows:
1) A static HTML file ("xdomain_proxy.html") is served from courses.edx.org.
The file includes JavaScript and a domain whitelist.
2) The course details page (on edx.org) creates an invisible iframe
that loads the proxy HTML file.
3) A JS shim library on the course details page intercepts
AJAX requests and communicates with JavaScript on the iframed page.
The iframed page then proxies the request to the LMS.
Since the iframed page is served from courses.edx.org,
this is a same-domain request, so all cookies for the domain
are sent along with the request.
You can enable this feature and configure the domain whitelist
using Django admin.
"""
config = XDomainProxyConfiguration.current()
if not config.enabled:
return HttpResponseNotFound()
allowed_domains = []
for domain in config.whitelist.split("\n"): # pylint: disable=no-member
if domain.strip():
allowed_domains.append(domain.strip())
if not allowed_domains:
log.warning(
u"No whitelist configured for cross-domain proxy. "
u"You can configure the whitelist in Django Admin "
u"using the XDomainProxyConfiguration model."
)
return HttpResponseNotFound()
context = {
'xdomain_masters': json.dumps({
domain: '*'
for domain in allowed_domains
})
}
return render_to_response('cors_csrf/xdomain_proxy.html', context)
<%namespace name='static' file='../static_content.html'/>
<!DOCTYPE HTML>
<script src="${static.url('js/vendor/xdomain.min.js')}"></script>
<script>xdomain.masters(${xdomain_masters});</script>
......@@ -1797,13 +1797,18 @@ if FEATURES.get('AUTH_USE_CAS'):
INSTALLED_APPS += ('django_cas',)
MIDDLEWARE_CLASSES += ('django_cas.middleware.CASMiddleware',)
############# CORS headers for cross-domain requests #################
############# Cross-domain requests #################
if FEATURES.get('ENABLE_CORS_HEADERS'):
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = ()
CORS_ORIGIN_ALLOW_ALL = False
# Default cache expiration for the cross-domain proxy HTML page.
# This is a static page that can be iframed into an external page
# to simulate cross-domain requests.
XDOMAIN_PROXY_CACHE_TIMEOUT = 60 * 15
###################### Registration ##################################
# For each of the fields, give one of the following values:
......
......@@ -613,6 +613,11 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
url(r'^certificates/html', 'certificates.views.render_html_view', name='cert_html_view'),
)
# XDomain proxy
urlpatterns += (
url(r'^xdomain_proxy.html$', 'cors_csrf.views.xdomain_proxy', name='xdomain_proxy'),
)
urlpatterns = patterns(*urlpatterns)
if settings.DEBUG:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment