From 645f9c2c7abca5587eb769858c31cf6c7aeaee22 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 26 Mar 2015 12:22:49 -0400 Subject: [PATCH] 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. --- common/djangoapps/cors_csrf/admin.py | 7 ++ .../cors_csrf/migrations/0001_initial.py | 74 +++++++++++++++++++ .../cors_csrf/migrations/__init__.py | 0 common/djangoapps/cors_csrf/models.py | 19 +++++ .../djangoapps/cors_csrf/tests/test_views.py | 72 ++++++++++++++++++ common/djangoapps/cors_csrf/views.py | 72 ++++++++++++++++++ common/templates/cors_csrf/xdomain_proxy.html | 5 ++ lms/envs/common.py | 7 +- lms/static/js/vendor/xdomain.min.js | 3 + lms/urls.py | 5 ++ 10 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 common/djangoapps/cors_csrf/admin.py create mode 100644 common/djangoapps/cors_csrf/migrations/0001_initial.py create mode 100644 common/djangoapps/cors_csrf/migrations/__init__.py create mode 100644 common/djangoapps/cors_csrf/tests/test_views.py create mode 100644 common/djangoapps/cors_csrf/views.py create mode 100644 common/templates/cors_csrf/xdomain_proxy.html create mode 100644 lms/static/js/vendor/xdomain.min.js diff --git a/common/djangoapps/cors_csrf/admin.py b/common/djangoapps/cors_csrf/admin.py new file mode 100644 index 0000000000..b7e4153a39 --- /dev/null +++ b/common/djangoapps/cors_csrf/admin.py @@ -0,0 +1,7 @@ +"""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) diff --git a/common/djangoapps/cors_csrf/migrations/0001_initial.py b/common/djangoapps/cors_csrf/migrations/0001_initial.py new file mode 100644 index 0000000000..bdc4e752ca --- /dev/null +++ b/common/djangoapps/cors_csrf/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# -*- 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'] diff --git a/common/djangoapps/cors_csrf/migrations/__init__.py b/common/djangoapps/cors_csrf/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/cors_csrf/models.py b/common/djangoapps/cors_csrf/models.py index e69de29bb2..61a39046bd 100644 --- a/common/djangoapps/cors_csrf/models.py +++ b/common/djangoapps/cors_csrf/models.py @@ -0,0 +1,19 @@ +"""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." + ) + ) diff --git a/common/djangoapps/cors_csrf/tests/test_views.py b/common/djangoapps/cors_csrf/tests/test_views.py new file mode 100644 index 0000000000..c706096d71 --- /dev/null +++ b/common/djangoapps/cors_csrf/tests/test_views.py @@ -0,0 +1,72 @@ +"""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) diff --git a/common/djangoapps/cors_csrf/views.py b/common/djangoapps/cors_csrf/views.py new file mode 100644 index 0000000000..29a7a294ff --- /dev/null +++ b/common/djangoapps/cors_csrf/views.py @@ -0,0 +1,72 @@ +"""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) diff --git a/common/templates/cors_csrf/xdomain_proxy.html b/common/templates/cors_csrf/xdomain_proxy.html new file mode 100644 index 0000000000..66d1a4a31a --- /dev/null +++ b/common/templates/cors_csrf/xdomain_proxy.html @@ -0,0 +1,5 @@ +<%namespace name='static' file='../static_content.html'/> + + + + diff --git a/lms/envs/common.py b/lms/envs/common.py index 5ed032e1f5..c36731e15c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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: diff --git a/lms/static/js/vendor/xdomain.min.js b/lms/static/js/vendor/xdomain.min.js new file mode 100644 index 0000000000..e5a1e17d9e --- /dev/null +++ b/lms/static/js/vendor/xdomain.min.js @@ -0,0 +1,3 @@ +// XDomain - v0.6.17 - https://github.com/jpillora/xdomain +// Jaime Pillora - MIT Copyright 2014 +(function(a,b){(function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};r=a.document,d="before",c="after",l="readyState",k="addEventListener",j="removeEventListener",g="dispatchEvent",o="XMLHttpRequest",h="FormData",m=["load","loadend","loadstart"],e=["progress","abort","error","timeout"],u=parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase())||[])[1]),isNaN(u)&&(u=parseInt((/trident\/.*; rv:(\d+)/.exec(navigator.userAgent.toLowerCase())||[])[1])),(y=Array.prototype).indexOf||(y.indexOf=function(a){var b,c,d,e;for(b=d=0,e=this.length;e>d;b=++d)if(c=this[b],c===a)return b;return-1}),w=function(a,b){return Array.prototype.slice.call(a,b)},q=function(a){return"returnValue"===a||"totalSize"===a||"position"===a},t=function(a,b){var c,d;for(c in a)if(d=a[c],!q(c))try{b[c]=a[c]}catch(e){}return b},v=function(a,b,c){var d,e,f,h;for(e=function(a){return function(d){var e,f,h;e={};for(f in d)q(f)||(h=d[f],e[f]=h===b?c:h);return c[g](a,e)}},f=0,h=a.length;h>f;f++)d=a[f],b["on"+d]=e(d)},s=function(a){var b;if(null!=r.createEventObject)return b=r.createEventObject(),b.type=a,b;try{return new Event(a)}catch(c){return{type:a}}},f=function(a){var c,d,e;return d={},e=function(a){return d[a]||[]},c={},c[k]=function(a,c,f){d[a]=e(a),d[a].indexOf(c)>=0||(f=f===b?d[a].length:f,d[a].splice(f,0,c))},c[j]=function(a,c){var f;return a===b?void(d={}):(c===b&&(d[a]=[]),f=e(a).indexOf(c),void(-1!==f&&e(a).splice(f,1)))},c[g]=function(){var d,f,g,h,i,j,k,l;for(d=w(arguments),f=d.shift(),a||(d[0]=t(d[0],s(f))),h=c["on"+f],h&&h.apply(b,d),l=e(f).concat(e("*")),g=j=0,k=l.length;k>j;g=++j)i=l[g],i.apply(b,d)},a&&(c.listeners=function(a){return w(e(a))},c.on=c[k],c.off=c[j],c.fire=c[g],c.once=function(a,b){var d;return d=function(){return c.off(a,d),b.apply(null,arguments)},c.on(a,d)},c.destroy=function(){return d={}}),c},x=f(!0),x.EventEmitter=f,x[d]=function(a,b){if(a.length<1||a.length>2)throw"invalid hook";return x[k](d,a,b)},x[c]=function(a,b){if(a.length<2||a.length>3)throw"invalid hook";return x[k](c,a,b)},x.enable=function(){a[o]=n},x.disable=function(){a[o]=x[o]},p=x.headers=function(a,b){var c,d,e,f,g,h,i,j,k;switch(null==b&&(b={}),typeof a){case"object":d=[];for(e in a)g=a[e],f=e.toLowerCase(),d.push(""+f+": "+g);return d.join("\n");case"string":for(d=a.split("\n"),i=0,j=d.length;j>i;i++)c=d[i],/([^:]+):\s*(.+)/.test(c)&&(f=null!=(k=RegExp.$1)?k.toLowerCase():void 0,h=RegExp.$2,null==b[f]&&(b[f]=h));return b}},i=a[h],i&&(x[h]=i,a[h]=function(a){var b;this.fd=a?new i(a):new i,this.form=a,b=[],Object.defineProperty(this,"entries",{get:function(){var c;return c=a?w(a.querySelectorAll("input,select")).filter(function(a){var b;return"checkbox"!==(b=a.type)&&"radio"!==b||a.checked}).map(function(a){return[a.name,"file"===a.type?a.files:a.value]}):[],c.concat(b)}}),this.append=function(a){return function(){var c;return c=w(arguments),b.push(c),a.fd.append.apply(a.fd,c)}}(this)}),x[o]=a[o],n=a[o]=function(){var b,i,j,n,q,r,s,w,y,A,B,C,D,E,F,G,H;return b=-1,H=new x[o],A={},D=null,r=void 0,E=void 0,B=void 0,y=function(){var a,c,d,e;if(B.status=D||H.status,D===b&&10>u||(B.statusText=H.statusText),D!==b){e=p(H.getAllResponseHeaders());for(a in e)d=e[a],B.headers[a]||(c=a.toLowerCase(),B.headers[c]=d)}},w=function(){"responseText"in H&&(B.text=H.responseText),"responseXML"in H&&(B.xml=H.responseXML),"response"in H&&(B.data=H.response)},G=function(){q.status=B.status,q.statusText=B.statusText},F=function(){"text"in B&&(q.responseText=B.text),"xml"in B&&(q.responseXML=B.xml),"data"in B&&(q.response=B.data)},n=function(a){for(;a>i&&4>i;)q[l]=++i,1===i&&q[g]("loadstart",{}),2===i&&G(),4===i&&(G(),F()),q[g]("readystatechange",{}),4===i&&setTimeout(j,0)},j=function(){r||q[g]("load",{}),q[g]("loadend",{}),r&&(q[l]=0)},i=0,C=function(a){var b,d;return 4!==a?void n(a):(b=x.listeners(c),d=function(){var a;return b.length?(a=b.shift(),2===a.length?(a(A,B),d()):3===a.length&&A.async?a(A,B,d):d()):n(4)},void d())},q=A.xhr=f(),H.onreadystatechange=function(){try{2===H[l]&&y()}catch(a){}4===H[l]&&(E=!1,y(),w()),C(H[l])},s=function(){r=!0},q[k]("error",s),q[k]("timeout",s),q[k]("abort",s),q[k]("progress",function(){3>i?C(3):q[g]("readystatechange",{})}),v(e,H,q),("withCredentials"in H||x.addWithCredentials)&&(q.withCredentials=!1),q.status=0,q.open=function(a,b,c,d,e){i=0,r=!1,E=!1,A.headers={},A.headerNames={},A.status=0,B={},B.headers={},A.method=a,A.url=b,A.async=c!==!1,A.user=d,A.pass=e,C(1)},q.send=function(b){var c,e,f,g,i,j,k,l;for(l=["type","timeout","withCredentials"],j=0,k=l.length;k>j;j++)e=l[j],f="type"===e?"responseType":e,f in q&&(A[e]=q[f]);A.body=b,i=function(){var b,c,d,g,i,j;for(E=!0,H.open(A.method,A.url,A.async,A.user,A.pass),i=["type","timeout","withCredentials"],d=0,g=i.length;g>d;d++)e=i[d],f="type"===e?"responseType":e,e in A&&(H[f]=A[e]);j=A.headers;for(b in j)c=j[b],H.setRequestHeader(b,c);a[h]&&A.body instanceof a[h]&&(A.body=A.body.fd),H.send(A.body)},c=x.listeners(d),(g=function(){var a,b;return c.length?(a=function(a){return"object"!=typeof a||"number"!=typeof a.status&&"number"!=typeof B.status?void g():(t(a,B),z.call(a,"data")<0&&(a.data=a.response||a.text),void C(4))},a.head=function(a){return t(a,B),C(2)},a.progress=function(a){return t(a,B),C(3)},b=c.shift(),1===b.length?a(b(A)):2===b.length&&A.async?b(A,a):a()):i()})()},q.abort=function(){D=b,E?H.abort():q[g]("abort",{})},q.setRequestHeader=function(a,b){var c,d;c=null!=a?a.toLowerCase():void 0,d=A.headerNames[c]=A.headerNames[c]||a,A.headers[d]&&(b=A.headers[d]+", "+b),A.headers[d]=b},q.getResponseHeader=function(a){var b;return b=null!=a?a.toLowerCase():void 0,B.headers[b]},q.getAllResponseHeaders=function(){return p(B.headers)},H.overrideMimeType&&(q.overrideMimeType=function(){return H.overrideMimeType.apply(H,arguments)}),H.upload&&(q.upload=A.upload=f(),v(e.concat(m),H.upload,q.upload)),q},"function"==typeof this.define&&this.define.amd?define("xhook",[],function(){return x}):(this.exports||this).xhook=x}).call(this,a);var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P;E=null,g=function(a){var b,c;null===E&&(E={},s());for(b in a)c=a[b],y("adding slave: "+b),E[b]=c},o={},p=function(a,b){var c;return o[a]?o[a]:(c=l.createElement("iframe"),c.id=c.name=q(),y("creating iframe "+c.id),c.src=""+a+b,c.setAttribute("style","display:none;"),l.body.appendChild(c),o[a]=c.contentWindow)},s=function(){var a,b,c;return b=function(a,b){var c,d,e,f,g;return e=a[0],f=a[1],c=u(f,"Blob"),d=u(f,"File"),c||d?(g=new FileReader,g.onload=function(){return a[1]=null,d&&(a[2]=f.name),b(["XD_BLOB",a,this.result,f.type])},g.readAsArrayBuffer(f),1):0},a=function(a,c){var d;a.forEach(function(b,c){var d,e,f,g,h;if(e=b[0],f=b[1],u(f,"FileList"))for(a.splice(c,1),g=0,h=f.length;h>g;g++)d=f[g],a.splice(c,0,[e,d])}),d=0,a.forEach(function(e,f){d+=b(e,function(b){a[f]=b,0===--d&&c()})}),0===d&&c()},c=function(b,c){var d,e,f;return c.on("xhr-event",function(){return b.xhr.dispatchEvent.apply(null,arguments)}),c.on("xhr-upload-event",function(){return b.xhr.upload.dispatchEvent.apply(null,arguments)}),e=I(b),e.headers=b.headers,b.withCredentials&&(e.credentials=l.cookie),f=function(){return c.emit("request",e)},b.body&&(e.body=b.body,u(e.body,"FormData"))?(d=e.body.entries,e.body=["XD_FD",d],void a(d,f)):void f()},"addWithCredentials"in M||(M.addWithCredentials=!0),M.before(function(a,b){var d,e,f;return e=C(a.url),e&&e.origin!==k?E[e.origin]?(y("proxying request to slave: '"+e.origin+"'"),a.async===!1?(K("sync not supported"),b()):(d=p(e.origin,E[e.origin]),f=h(d),f.on("response",function(a){return b(a),f.close()}),a.xhr.addEventListener("abort",function(){return f.emit("abort")}),void(f.ready?c(a,f):f.once("ready",function(){return c(a,f)})))):(e&&y("no slave matching: '"+e.origin+"'"),b()):b()})},A=null,f=function(a){var b,c;null===A&&(A={},t());for(b in a)c=a[b],y("adding master: "+b),A[b]=c},t=function(){return w(function(a,b){var c,d,e,f;"null"===a&&(a="*"),e=null;for(c in A){f=A[c];try{if(d=J(c),d.test(a)){e=J(f);break}}catch(g){}}return e?(b.once("request",function(a){var c,d,f,g,h,i,j,k,l,m,n;if(y("request: "+a.method+" "+a.url),i=C(a.url),!i||!e.test(i.path))return K("blocked request to path: '"+i.path+"' by regex: "+e),void b.close();k=new XMLHttpRequest,k.open(a.method,a.url),k.addEventListener("*",function(a){return b.emit("xhr-event",a.type,I(a))}),k.upload&&k.upload.addEventListener("*",function(a){return b.emit("xhr-upload-event",a.type,I(a))}),b.once("abort",function(){return k.abort()}),k.onreadystatechange=function(){var a;if(4===k.readyState){a={status:k.status,statusText:k.statusText,data:k.response,headers:M.headers(k.getAllResponseHeaders())};try{a.text=k.responseText}catch(c){}return b.emit("response",a)}},a.withCredentials&&(a.headers["XDomain-Cookie"]=a.credentials),a.timeout&&(k.timeout=a.timeout),a.type&&(k.responseType=a.type),n=a.headers;for(h in n)j=n[h],k.setRequestHeader(h,j);if(a.body instanceof Array&&"XD_FD"===a.body[0]){for(g=new M.FormData,f=a.body[1],l=0,m=f.length;m>l;l++)c=f[l],"XD_BLOB"===c[0]&&4===c.length&&(d=new Blob([c[2]],{type:c[3]}),c=c[1],c[1]=d),g.append.apply(g,c);a.body=g}k.send(a.body||null)}),void y("slave listening for requests on socket: "+b.id)):void K("blocked request from: '"+a+"'")}),a===a.parent?K("slaves must be in an iframe"):a.parent.postMessage("XDPING_"+d,"*")},B=function(b){return l.addEventListener?a.addEventListener("message",b):a.attachEvent("onmessage",b)},e="XD_CHECK",r=null,G={},v=!0,H=function(){return B(function(a){var c,e,f,g;if(c=a.data,"string"==typeof c){if(/^XDPING(_(V\d+))?$/.test(c)&&RegExp.$2!==d)return K("your master is not compatible with your slave, check your xdomain.js version");if(/^xdomain-/.test(c))c=c.split(",");else if(v)try{c=JSON.parse(c)}catch(h){return}}if(c instanceof Array&&(f=c.shift(),/^xdomain-/.test(f)&&(g=G[f],null!==g))){if(g===b){if(!r)return;g=j(f,a.source),r(a.origin,g)}e="string"==typeof c[1]?": '"+c[1]+"'":"",y("receive socket: "+f+": '"+c[0]+"'"+e),g.fire.apply(g,c)}})},j=function(a,b){var d,f,g,h,i,j;return i=!1,j=G[a]=M.EventEmitter(!0),j.id=a,j.once("close",function(){return j.destroy(),j.close()}),h=[],j.emit=function(){var b,c;b=F(arguments),c="string"==typeof b[1]?": '"+b[1]+"'":"",y("send socket: "+a+": "+b[0]+c),b.unshift(a),i?g(b):h.push(b)},g=function(a){v&&(a=JSON.stringify(a)),b.postMessage(a,"*")},j.close=function(){j.emit("close"),y("close socket: "+a),G[a]=null},j.once(e,function(b){for(v="string"==typeof b,i=j.ready=!0,j.emit("ready"),y("ready socket: "+a+" (emit #"+h.length+" pending)");h.length;)g(h.shift())}),f=0,d=function(){return function(){b.postMessage([a,e,{}],"*"),i||(f++>=L.timeout/c?(K("Timeout waiting on iframe socket"),m.fire("timeout"),j.fire("abort")):setTimeout(d,c))}}(this),setTimeout(d),y("new socket: "+a),j},h=function(a){var b;return b=j(q(),a)},w=function(a){r=a},M=(this.exports||this).xhook,L=function(a){a&&(a.masters&&f(a.masters),a.slaves&&g(a.slaves))},L.masters=f,L.slaves=g,L.debug=!1,L.timeout=15e3,c=100,l=a.document,x=a.location,k=L.origin=x.protocol+"//"+x.host,q=function(){return"xdomain-"+Math.round(Math.random()*Math.pow(2,32)).toString(16)},F=function(a,b){return Array.prototype.slice.call(a,b)},i=a.console||{},m=null,D=function(){m=M.EventEmitter(!0),L.on=m.on},M&&D(),z=function(a){return function(b){b="xdomain ("+k+"): "+b,m.fire(a,b),("log"!==a||L.debug)&&(a in L?L[a](b):a in i?i[a](b):"warn"===a&&alert(b))}},y=z("log"),K=z("warn"),P=["postMessage","JSON"];for(N=0,O=P.length;O>N;N++)if(n=P[N],!a[n])return void K("requires '"+n+"' and this browser does not support it");u=function(b,c){return c in a?b instanceof a[c]:!1},d="V1",C=L.parseUrl=function(a){return/^((https?:)?\/\/[^\/\?]+)(\/.*)?/.test(a)?{origin:(RegExp.$2?"":x.protocol)+RegExp.$1,path:RegExp.$3}:(y("failed to parse absolute url: "+a),null)},J=function(a){var b;return a instanceof RegExp?a:(b=a.toString().replace(/\W/g,function(a){return"\\"+a}).replace(/\\\*/g,".*"),new RegExp("^"+b+"$"))},I=function(a){var b,c,d,e;b={};for(c in a)"returnValue"!==c&&(d=a[c],"function"!=(e=typeof d)&&"object"!==e&&(b[c]=d));return b},function(){var a,b,c,d,e,h,i,j,k,m,n;for(a={debug:function(a){return"string"==typeof a?L.debug="false"!==a:void 0},slave:function(a){var b,c;if(a&&(b=C(a)))return c={},c[b.origin]=b.path,g(c)},master:function(a){var b,c;if(a&&(c="*"===a?{origin:"*",path:"*"}:C(a)))return b={},b[c.origin]=c.path.replace(/^\//,"")?c.path:"*",f(b)}},m=l.getElementsByTagName("script"),h=0,j=m.length;j>h;h++)if(e=m[h],/xdomain/.test(e.src))for(n=["","data-"],i=0,k=n.length;k>i;i++){d=n[i];for(c in a)(b=a[c])(e.getAttribute(d+c))}}(),H(),"function"==typeof this.define&&this.define.amd?define("xdomain",["xhook"],function(a){return M=a,D(),L}):(this.exports||this).xdomain=L}).call(this,window); \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index 97a9a74e4e..512cb159ba 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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: