diff --git a/client/sagas/execute-challenge-saga.js b/client/sagas/execute-challenge-saga.js index de914dccbe..ab7b9ac8a4 100644 --- a/client/sagas/execute-challenge-saga.js +++ b/client/sagas/execute-challenge-saga.js @@ -21,22 +21,27 @@ function createFileStream(files = {}) { ); } -const jQuery = { +const globalRequires = [{ + link: 'https://cdnjs.cloudflare.com/' + + 'ajax/libs/normalize/4.2.0/normalize.min.css' +}, { src: '/bower_components/jquery/dist/jquery.js', script: true, - type: 'global' -}; + type: 'global', + crossDomain: false +}]; const scriptCache = new Map(); +const linkCache = new Map(); -function cacheScript({ src } = {}) { +function cacheScript({ src } = {}, crossDomain = true) { if (!src) { return Observable.throw(new Error('No source provided for script')); } if (scriptCache.has(src)) { return scriptCache.get(src); } - const script$ = ajax$(src) + const script$ = ajax$({ url: src, crossDomain }) .doOnNext(res => { if (res.status !== 200) { throw new Error('Request errror: ' + res.status); @@ -51,12 +56,37 @@ function cacheScript({ src } = {}) { return script$; } -const frameRunner$ = cacheScript({ src: '/js/frame-runner.js' }); +function cacheLink({ link } = {}, crossDomain = true) { + if (!link) { + return Observable.throw(new Error('No source provided for link')); + } + if (linkCache.has(link)) { + return linkCache.get(link); + } + const link$ = ajax$({ url: link, crossDomain }) + .doOnNext(res => { + if (res.status !== 200) { + throw new Error('Request errror: ' + res.status); + } + }) + .map(({ response }) => response) + .map(script => ``) + .catch(createErrorObservable) + .shareReplay(); + + linkCache.set(link, link$); + return link$; +} + const htmlCatch = '\n'; const jsCatch = '\n;/*fcc*/'; export default function executeChallengeSaga(action$, getState) { + const frameRunner$ = cacheScript( + { src: '/js/frame-runner.js' }, + false + ); return action$ .filter(({ type }) => ( type === types.executeChallenge || @@ -64,7 +94,8 @@ export default function executeChallengeSaga(action$, getState) { )) .debounce(750) .flatMapLatest(({ type }) => { - const { files, required = [ jQuery ] } = getState().challengesApp; + const { files, required = [] } = getState().challengesApp; + const finalRequires = [...required, ...globalRequires ]; return createFileStream(files) ::throwers() ::transformers() @@ -88,10 +119,13 @@ export default function executeChallengeSaga(action$, getState) { }, '')) // add required scripts and links here .flatMap(source => { - const head$ = Observable.from(required) + const head$ = Observable.from(finalRequires) .flatMap(required => { if (required.script) { - return cacheScript(required); + return cacheScript(required, required.crossDomain); + } + if (required.link) { + return cacheLink(required, required.crossDomain); } return Observable.just(''); }) diff --git a/server/middlewares/csp.js b/server/middlewares/csp.js index 42a327f32a..93b3cc9921 100644 --- a/server/middlewares/csp.js +++ b/server/middlewares/csp.js @@ -15,7 +15,11 @@ if (process.env.NODE_ENV !== 'production') { export default function csp() { return helmet.contentSecurityPolicy({ directives: { - defaultSrc: trusted.concat('*.optimizely.com'), + defaultSrc: trusted.concat([ + '*.optimizely.com', + 'https://*.cloudflare.com', + '*.cloudflare.com' + ]), scriptSrc: [ "'unsafe-eval'", "'unsafe-inline'",