diff --git a/src/openwhisk/static.js b/src/openwhisk/static.js index 2c9c0b5dc..f3f1dcf9f 100644 --- a/src/openwhisk/static.js +++ b/src/openwhisk/static.js @@ -97,53 +97,54 @@ function isBinary(type) { return true; } -function rewriteImports(tree) { - tree.walkAtRules('import', (rule) => { - if (rule.name === 'import') { - const [url, queries] = space(rule.params); - const parsedurl = parser(url); - if (parsedurl.nodes - && parsedurl.nodes.length === 1 - && parsedurl.nodes[0].value === 'url' - && parsedurl.nodes[0].nodes - && parsedurl.nodes[0].nodes.length === 1 - && parsedurl.nodes[0].nodes[0].type === 'string' - && typeof parsedurl.nodes[0].nodes[0].value === 'string' - && typeof parsedurl.nodes[0].nodes[0].quote === 'string') { - const importuri = uri.parse(parsedurl.nodes[0].nodes[0].value); - const { quote } = parsedurl.nodes[0].nodes[0]; - if (importuri.reference === 'relative' && !importuri.query) { - rule.replaceWith(postcss.atRule({ - name: 'import', - params: `url(${quote}${importuri.path}${quote}) ${queries}`, - })); - } - } else if (parsedurl.nodes - && parsedurl.nodes[0].type === 'string' - && typeof parsedurl.nodes[0].value === 'string' - && typeof parsedurl.nodes[0].quote === 'string') { - const importuri = uri.parse(parsedurl.nodes[0].value); - const { quote } = parsedurl.nodes[0]; - if (importuri.reference === 'relative' && !importuri.query) { - rule.replaceWith(postcss.atRule({ - name: 'import', - params: `${quote}${importuri.path}${quote} ${queries}`, - })); +function rewriteCSS(css, base = '') { + function rewriteImports(tree) { + tree.walkAtRules('import', (rule) => { + if (rule.name === 'import') { + const [url, queries] = space(rule.params); + const parsedurl = parser(url); + if (parsedurl.nodes + && parsedurl.nodes.length === 1 + && parsedurl.nodes[0].value === 'url' + && parsedurl.nodes[0].nodes + && parsedurl.nodes[0].nodes.length === 1 + && parsedurl.nodes[0].nodes[0].type === 'string' + && typeof parsedurl.nodes[0].nodes[0].value === 'string' + && typeof parsedurl.nodes[0].nodes[0].quote === 'string') { + const importuri = uri.parse(parsedurl.nodes[0].nodes[0].value); + const { quote } = parsedurl.nodes[0].nodes[0]; + if (importuri.reference === 'relative' && !importuri.query) { + rule.replaceWith(postcss.atRule({ + name: 'import', + params: `url(${quote}${importuri.path}${quote}) ${queries}`, + })); + } + } else if (parsedurl.nodes + && parsedurl.nodes[0].type === 'string' + && typeof parsedurl.nodes[0].value === 'string' + && typeof parsedurl.nodes[0].quote === 'string') { + const importuri = uri.parse(parsedurl.nodes[0].value); + const { quote } = parsedurl.nodes[0]; + if (importuri.reference === 'relative' && !importuri.query) { + rule.replaceWith(postcss.atRule({ + name: 'import', + params: `${quote}${importuri.path}${quote} ${queries}`, + })); + } } } - } - }); - return tree; -} + }); + return tree; + } + -function rewriteCSS(css) { const processor = postcss() .use(rewriteImports) .use(postcssurl({ url: (asset) => { // TODO pass in request URL and make it absolute. if (asset.search === '' && asset.absolutePath !== '.' && asset.relativePath !== '.') { - return `${asset.relativePath}`; + return `${asset.relativePath}`; } return asset.url; }, @@ -151,7 +152,7 @@ function rewriteCSS(css) { return processor.process(css, { from: undefined }).then(result => result.css); } -function rewriteJavaScript(javascript) { +function rewriteJavaScript(javascript, base = '') { const importmap = {}; function rewriteJSImports(bab) { @@ -169,7 +170,7 @@ function rewriteJavaScript(javascript) { const { specifiers } = path.node; // console.log(srcuri); const h = ohash(srcuri.path); - importmap[h] = `${path.node.source.value}`; + importmap[h] = `${path.node.source.value}`; path.replaceWith(t.importDeclaration(specifiers, t.stringLiteral(h))); } } @@ -194,7 +195,7 @@ function isJSON(type) { return !!type.match(/json/); } -function getBody(type, responsebody, esi = false) { +function getBody(type, responsebody, esi = false, entry) { if (isBinary(type)) { return Buffer.from(responsebody).toString('base64'); } @@ -202,10 +203,10 @@ function getBody(type, responsebody, esi = false) { return JSON.parse(responsebody); } if (esi && isCSS(type)) { - return rewriteCSS(responsebody.toString()); + return rewriteCSS(responsebody.toString(), entry); } if (esi && isJavaScript(type)) { - return rewriteJavaScript(responsebody.toString()); + return rewriteJavaScript(responsebody.toString(), entry); } return responsebody.toString(); } @@ -229,6 +230,7 @@ function deliverPlain(owner, repo, ref, entry, root, esi = false) { const cleanentry = (`${root}/${entry}`).replace(/^\//, '').replace(/[/]+/g, '/'); console.log('deliverPlain()', owner, repo, ref, cleanentry); const url = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${cleanentry}`; + console.log(url); const rawopts = { url, headers: { @@ -243,14 +245,14 @@ function deliverPlain(owner, repo, ref, entry, root, esi = false) { const size = parseInt(response.headers['content-length'], 10); console.log('size', size); if (size < REDIRECT_LIMIT) { - const body = await getBody(type, response.body, esi); + const body = await getBody(type, response.body, esi, entry); console.log(`delivering file ${cleanentry} type ${type} binary: ${isBinary(type)}`); return { statusCode: 200, headers: addHeaders({ 'Content-Type': type, 'X-Static': 'Raw/Static', - 'X-ESI': esi ? 'enabled' : undefined + 'X-ESI': esi ? 'enabled' : undefined, }, ref, response.body), body, }; @@ -324,12 +326,14 @@ async function main({ }) { console.log('main()', owner, repo, ref, path, entry, strain, plain, allow, deny, root); - if (blacklisted(path, allow, deny) || blacklisted(entry, allow, deny)) { + const file = uri.normalize(entry); + console.log(file); + if (blacklisted(file, allow, deny) || blacklisted(file, allow, deny)) { return forbidden(); } if (plain) { - return deliverPlain(owner, repo, ref, entry, root, esi); + return deliverPlain(owner, repo, ref, file, root, esi); } return forbidden(); diff --git a/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs-anywhere_508785622/recording.har b/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs-anywhere_508785622/recording.har new file mode 100644 index 000000000..0564006e1 --- /dev/null +++ b/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs-anywhere_508785622/recording.har @@ -0,0 +1,161 @@ +{ + "log": { + "_recordingName": "Static Delivery Action #unittest/main() normalizes URLs anywhere", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "2.3.0" + }, + "entries": [ + { + "_id": "eb3115506cd378743495fae6ca116e5e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "user-agent", + "value": "Project Helix Static" + }, + { + "name": "host", + "value": "raw.githubusercontent.com" + } + ], + "headersSize": 170, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://raw.githubusercontent.com/adobe/helix-cli/master/demos/simple/htdocs/style.css" + }, + "response": { + "bodySize": 711, + "content": { + "mimeType": "text/plain; charset=utf-8", + "size": 711, + "text": "/*\n * Copyright 2018 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\nbody {\n padding: 8px;\n background-color: white;\n font-family: Arial, sans-serif;\n}" + }, + "cookies": [], + "headers": [ + { + "name": "content-security-policy", + "value": "default-src 'none'; style-src 'unsafe-inline'; sandbox" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-frame-options", + "value": "deny" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + }, + { + "name": "etag", + "value": "\"ffb46f86cbf2385c8310afe1a1cc1fc2f81ddf0f\"" + }, + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "cache-control", + "value": "max-age=300" + }, + { + "name": "x-geo-block-list", + "value": "" + }, + { + "name": "x-github-request-id", + "value": "92E4:692F:6D76F:81D1A:5CC2D895" + }, + { + "name": "content-length", + "value": "711" + }, + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "date", + "value": "Fri, 26 Apr 2019 10:10:13 GMT" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "x-served-by", + "value": "cache-fra19123-FRA" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "1" + }, + { + "name": "x-timer", + "value": "S1556273413.375203,VS0,VE0" + }, + { + "name": "vary", + "value": "Authorization,Accept-Encoding" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "x-fastly-request-id", + "value": "9113b51dfbcbbdd78b5d8dd7c73df85b14f81b61" + }, + { + "name": "expires", + "value": "Fri, 26 Apr 2019 10:15:13 GMT" + }, + { + "name": "source-age", + "value": "111" + } + ], + "headersSize": 816, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2019-04-26T10:10:13.256Z", + "time": 156, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 156 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs-in-rewritten-Javascript_420932251/recording.har b/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs-in-rewritten-Javascript_420932251/recording.har new file mode 100644 index 000000000..04b5ee2c6 --- /dev/null +++ b/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs-in-rewritten-Javascript_420932251/recording.har @@ -0,0 +1,161 @@ +{ + "log": { + "_recordingName": "Static Delivery Action #unittest/main() normalizes URLs in rewritten Javascript", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "2.3.0" + }, + "entries": [ + { + "_id": "6cafaa07ff38b12748719008b3dcd2e3", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "user-agent", + "value": "Project Helix Static" + }, + { + "name": "host", + "value": "raw.githubusercontent.com" + } + ], + "headersSize": 160, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://raw.githubusercontent.com/trieloff/helix-demo/master/htdocs/index.js" + }, + "response": { + "bodySize": 620, + "content": { + "mimeType": "text/plain; charset=utf-8", + "size": 620, + "text": "import barba from './web_modules/@barba--core.js';\nimport prefetch from './web_modules/@barba--prefetch.js';\n\n// tells barba to use the prefetch module\nbarba.use(prefetch);\n\n// Basic default transition, with no rules and minimal hooks…\nbarba.init({\n transitions: [{\n leave({ current, next, trigger }) {\n // Do something with `current.container` for your leave transition\n // then return a promise or use `this.async()`\n },\n enter({ current, next, trigger }) {\n // Do something with `next.container` for your enter transition\n // then return a promise or use `this.async()`\n }\n }],\n});" + }, + "cookies": [], + "headers": [ + { + "name": "content-security-policy", + "value": "default-src 'none'; style-src 'unsafe-inline'; sandbox" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-frame-options", + "value": "deny" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + }, + { + "name": "etag", + "value": "\"236d20fd3af45d02e50858af1fb752fe1c96cf58\"" + }, + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "cache-control", + "value": "max-age=300" + }, + { + "name": "x-geo-block-list", + "value": "" + }, + { + "name": "x-github-request-id", + "value": "460C:256B:24883D:294586:5CC2D9E9" + }, + { + "name": "content-length", + "value": "620" + }, + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "date", + "value": "Fri, 26 Apr 2019 10:14:01 GMT" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "x-served-by", + "value": "cache-hhn1538-HHN" + }, + { + "name": "x-cache", + "value": "MISS" + }, + { + "name": "x-cache-hits", + "value": "0" + }, + { + "name": "x-timer", + "value": "S1556273642.766573,VS0,VE143" + }, + { + "name": "vary", + "value": "Authorization,Accept-Encoding" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "x-fastly-request-id", + "value": "65a3779a8589561d771bf8a86b03283dcc859580" + }, + { + "name": "expires", + "value": "Fri, 26 Apr 2019 10:19:01 GMT" + }, + { + "name": "source-age", + "value": "0" + } + ], + "headersSize": 818, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2019-04-26T10:14:01.386Z", + "time": 560, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 560 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs_1747864213/recording.har b/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs_1747864213/recording.har new file mode 100644 index 000000000..fc3956e81 --- /dev/null +++ b/test/fixtures/recordings/Static-Delivery-Action-unittest_2612775662/main-normalizes-URLs_1747864213/recording.har @@ -0,0 +1,161 @@ +{ + "log": { + "_recordingName": "Static Delivery Action #unittest/main() normalizes URLs", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "2.3.0" + }, + "entries": [ + { + "_id": "eb3115506cd378743495fae6ca116e5e", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 0, + "cookies": [], + "headers": [ + { + "name": "user-agent", + "value": "Project Helix Static" + }, + { + "name": "host", + "value": "raw.githubusercontent.com" + } + ], + "headersSize": 170, + "httpVersion": "HTTP/1.1", + "method": "GET", + "queryString": [], + "url": "https://raw.githubusercontent.com/adobe/helix-cli/master/demos/simple/htdocs/style.css" + }, + "response": { + "bodySize": 711, + "content": { + "mimeType": "text/plain; charset=utf-8", + "size": 711, + "text": "/*\n * Copyright 2018 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\nbody {\n padding: 8px;\n background-color: white;\n font-family: Arial, sans-serif;\n}" + }, + "cookies": [], + "headers": [ + { + "name": "content-security-policy", + "value": "default-src 'none'; style-src 'unsafe-inline'; sandbox" + }, + { + "name": "strict-transport-security", + "value": "max-age=31536000" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-frame-options", + "value": "deny" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + }, + { + "name": "etag", + "value": "\"ffb46f86cbf2385c8310afe1a1cc1fc2f81ddf0f\"" + }, + { + "name": "content-type", + "value": "text/plain; charset=utf-8" + }, + { + "name": "cache-control", + "value": "max-age=300" + }, + { + "name": "x-geo-block-list", + "value": "" + }, + { + "name": "x-github-request-id", + "value": "92E4:692F:6D76F:81D1A:5CC2D895" + }, + { + "name": "content-length", + "value": "711" + }, + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "date", + "value": "Fri, 26 Apr 2019 10:08:21 GMT" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "connection", + "value": "close" + }, + { + "name": "x-served-by", + "value": "cache-fra19146-FRA" + }, + { + "name": "x-cache", + "value": "MISS" + }, + { + "name": "x-cache-hits", + "value": "0" + }, + { + "name": "x-timer", + "value": "S1556273302.767414,VS0,VE159" + }, + { + "name": "vary", + "value": "Authorization,Accept-Encoding" + }, + { + "name": "access-control-allow-origin", + "value": "*" + }, + { + "name": "x-fastly-request-id", + "value": "57dbac3675ae149b46f44dfbb73ed01ae22562b1" + }, + { + "name": "expires", + "value": "Fri, 26 Apr 2019 10:13:21 GMT" + }, + { + "name": "source-age", + "value": "0" + } + ], + "headersSize": 817, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2019-04-26T10:08:21.694Z", + "time": 277, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 277 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/test/testStatic.js b/test/testStatic.js index 220279f4a..a9928ef78 100644 --- a/test/testStatic.js +++ b/test/testStatic.js @@ -166,7 +166,7 @@ describe('CSS and JS Rewriting', () => { describe('Static Delivery Action #unittest', () => { setupPolly({ recordFailedRequests: true, - recordIfMissing: false, + recordIfMissing: true, logging: false, adapters: [NodeHttpAdapter], persister: FSPersister, @@ -249,6 +249,54 @@ describe('Static Delivery Action #unittest', () => { assert.ok(res.body.indexOf('Arial') > 0, true); }); + it('main() normalizes URLs', async () => { + const res = await index.main({ + owner: 'adobe', + repo: 'helix-cli', + entry: './demos/simple/htdocs/style.css', + plain: true, + }); + assert.ok(res.body.indexOf('Arial') > 0, true); + }); + + it('main() normalizes URLs anywhere', async () => { + const res = await index.main({ + owner: 'adobe', + repo: 'helix-cli', + entry: './demos/simple/test/../htdocs/style.css', + plain: true, + }); + assert.ok(res.body.indexOf('Arial') > 0, true); + }); + + it('main() normalizes URLs in rewritten Javascript', async () => { + const res = await index.main({ + owner: 'trieloff', + repo: 'helix-demo', + entry: '/index.js', + root: '/htdocs', + plain: true, + esi: true, + }); + assert.equal(res.body, `import barba from "./web_modules/@barba--core.js";import +prefetch from "./web_modules/@barba--prefetch.js"; + +// tells barba to use the prefetch module +barba.use(prefetch); + +// Basic default transition, with no rules and minimal hooks… +barba.init({ + transitions: [{ + leave({ current, next, trigger }) { + // Do something with \`current.container\` for your leave transition + // then return a promise or use \`this.async()\` + }, + enter({ current, next, trigger }) { + // Do something with \`next.container\` for your enter transition + // then return a promise or use \`this.async()\` + } }] });`); + }); + it('main() returns 403 if plain is false', async () => { const res = await index.main({ owner: 'adobe',