diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README | 11 | ||||
-rw-r--r-- | bg/ExternalLicenses.js | 45 | ||||
-rw-r--r-- | bg/ResponseProcessor.js | 35 | ||||
-rwxr-xr-x | build.sh | 15 | ||||
-rw-r--r-- | common/Test.js | 55 | ||||
-rw-r--r-- | content/externalLicenseChecker.js | 2 | ||||
-rw-r--r-- | html/common.css | 12 | ||||
-rw-r--r-- | html/display_panel/content/display-panel.html | 2 | ||||
-rw-r--r-- | html/display_panel/content/main_panel.js | 12 | ||||
-rw-r--r-- | html/display_panel/content/panel-styles.css | 3 | ||||
-rw-r--r-- | main_background.js | 193 | ||||
-rw-r--r-- | test/SpecRunner.html | 43 | ||||
-rw-r--r-- | test/resources/app-trilicensed.js | 1 | ||||
-rw-r--r-- | test/resources/index.html | 38 | ||||
-rw-r--r-- | test/resources/jquery.js | 1 | ||||
-rw-r--r-- | test/resources/jslicense.html | 25 | ||||
-rw-r--r-- | test/resources/proprietary.js | 1 | ||||
-rw-r--r-- | test/resources/tracker.js | 1 | ||||
-rw-r--r-- | test/spec/LibreJSSpec.js | 208 |
20 files changed, 590 insertions, 114 deletions
@@ -6,6 +6,7 @@ SDK LibreJS source/ # dependencies node_modules/ +test/lib/ # artifacts librejs.xpi @@ -15,6 +15,11 @@ To build the extension run: To build the extension plus create a .xpi package run: $ ./build.sh +To build the extension including the automated test suite (see TEST below) run: + $ ./build.sh -t + or + $ ./build.sh --test + Note: this build.sh script relies on no new source files being created. @@ -24,6 +29,12 @@ To debug this add-on on IceCat and other Firefox derivatives, browse to the spec LibreJS should work with other WebExtensions-compliant browsers; but currently, none of them meet the freedom standards of GNU, so no help will be provided for their usage. +TEST: + +An automated test suite runs automatically in its own tab whenever the extension +is loaded as a "temporary add-on" from about:debugging. +Otherwise (if included in the xpi, see BUILD above) it can be launched from the +UI by clicking the [Automated self test...] button. CONTACT: diff --git a/bg/ExternalLicenses.js b/bg/ExternalLicenses.js index 27d9223..33bcbf6 100644 --- a/bg/ExternalLicenses.js +++ b/bg/ExternalLicenses.js @@ -26,10 +26,28 @@ "use strict"; let licensesByLabel = new Map(); +let licensesByUrl = new Map(); { let {licenses} = require("../license_definitions"); - for (let l of Object.values(licenses).filter(l => l.identifier)) { - licensesByLabel.set(l.identifier, l); + let mapByLabel = (label, license) => licensesByLabel.set(label.toUpperCase(), license); + for (let [id, l] of Object.entries(licenses)) { + let {identifier, canonicalUrl, licenseName} = l; + if (identifier) { + mapByLabel(identifier, l); + } else { + l.identifier = id; + } + if (id !== identifier) { + mapByLabel(id, l); + } + if (licenseName) { + mapByLabel(licenseName, l); + } + if (Array.isArray(canonicalUrl)) { + for (let url of canonicalUrl) { + licensesByUrl.set(url, l); + } + } } } @@ -55,23 +73,26 @@ var ExternalLicenses = { return null; } scriptInfo.licenses = new Set(); - scriptInfo.allFree = true; scriptInfo.toString = function() { let licenseIds = [...this.licenses].map(l => l.identifier).sort().join(", "); return licenseIds - ? (this.allFree ? `Free license${this.licenses.length > 1 ? "s" : ""} (${licenseIds})` - : `Mixed free (${licenseIds}) and unknown licenses`) + ? `Free license${this.licenses.size > 1 ? "s" : ""} (${licenseIds})` : "Unknown license(s)"; } - - for (let {label} of scriptInfo.licenseLinks) { - if (licensesByLabel.has(label)) { - scriptInfo.licenses.add(licensesByLabel.get(label)); - } else { - scriptInfo.allFree = false; - break; + let match = (map, key) => { + if (map.has(key)) { + scriptInfo.licenses.add(map.get(key)); + return true; } + return false; + }; + + for (let {label, url} of scriptInfo.licenseLinks) { + match(licensesByLabel, label = label.trim().toUpperCase()) || + match(licensesByUrl, url) || + match(licensesByLabel, label.replace(/^GNU-|-(?:OR-LATER|ONLY)$/, '')); } + scriptInfo.free = scriptInfo.licenses.size > 0; return scriptInfo; }, diff --git a/bg/ResponseProcessor.js b/bg/ResponseProcessor.js index 1af8cd7..4443d90 100644 --- a/bg/ResponseProcessor.js +++ b/bg/ResponseProcessor.js @@ -33,7 +33,7 @@ class ResponseProcessor { static install(handler, types = ["main_frame", "sub_frame", "script"]) { if (listeners.has(handler)) return false; - let listener = + let listener = async request => await new ResponseTextFilter(request).process(handler); listeners.set(handler, listener); webRequestEvent.addListener( @@ -80,7 +80,7 @@ class ResponseTextFilter { if (handler.post) handler = handler.post; if (typeof handler !== "function") ResponseProcessor.ACCEPT; } - + let {requestId, responseHeaders} = request; let filter = browser.webRequest.filterResponseData(requestId); let buffer = []; @@ -90,11 +90,29 @@ class ResponseTextFilter { }; filter.onstop = async event => { - let decoder = metaData.createDecoder(); + let params = {stream: true}; - response.text = buffer.map( - chunk => decoder.decode(chunk, params)) - .join(''); + // concatenate chunks + let size = buffer.reduce((sum, chunk, n) => sum + chunk.byteLength, 0) + let allBytes = new Uint8Array(size); + let pos = 0; + for (let chunk of buffer) { + allBytes.set(new Uint8Array(chunk), pos); + pos += chunk.byteLength; + } + buffer = null; // allow garbage collection + if (allBytes.indexOf(0) !== -1) { + console.debug("Warning: zeroes in bytestream, probable cached encoding mismatch.", request); + if (request.type === "script") { + console.debug("It's a script, trying to refetch it."); + response.text = await (await fetch(request.url, {cache: "reload", credentials: "include"})).text(); + } else { + console.debug("It's a %s, trying to decode it as UTF-16.", request.type); + response.text = new TextDecoder("utf-16be").decode(allBytes); + } + } else { + response.text = metaData.createDecoder().decode(allBytes, {stream: true}); + } let editedText = null; try { editedText = await handler(response); @@ -108,10 +126,9 @@ class ResponseTextFilter { filter.write(new TextEncoder().encode(editedText)); } else { // ... otherwise pass all the raw bytes through - for (let chunk of buffer) filter.write(chunk); + filter.write(allBytes); } - - filter.disconnect(); + filter.close(); } return metaData.forceUTF8() ? {responseHeaders} : ResponseProcessor.ACCEPT;; @@ -1,6 +1,16 @@ PATH=$PATH:./node_modules/.bin which browserify > /dev/null || (echo "can not find browserify" && false) || exit +# prepare / update testing environment if needed +JASMINE_VER=3.2.1 +JASMINE_LIB="test/lib/jasmine-$JASMINE_VER" +if ! [ -d "$JASMINE_LIB" ]; then + mkdir -p test/lib + rm -rf test/lib/jasmine-* + JASMINE_URL="https://github.com/jasmine/jasmine/releases/download/v$JASMINE_VER/jasmine-standalone-$JASMINE_VER.zip" + curl -L -o "$JASMINE_LIB.zip" "$JASMINE_URL" && unzip -d test/ $JASMINE_LIB.zip lib/** + rm -f "$JASMINE_LIB.zip" +fi # Build the main file browserify main_background.js -o bundle.js @@ -9,7 +19,10 @@ browserify main_background.js -o bundle.js mkdir ./build_temp # Move source files to temp directory -cp -r icons ./build_temp +if [ "$1" == "-t" -o "$1" == "--test" ]; then + cp -r ./test ./build_temp +fi +cp -r ./icons ./build_temp cp -r ./html ./build_temp cp -r ./content ./build_temp cp -r ./common ./build_temp diff --git a/common/Test.js b/common/Test.js new file mode 100644 index 0000000..e272d1e --- /dev/null +++ b/common/Test.js @@ -0,0 +1,55 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; +var Test = (() => { + const RUNNER_URL = browser.extension.getURL("/test/SpecRunner.html"); + return { + /* + returns RUNNER_URL if it's a test-enabled build or an about:debugging + temporary extension session, null otherwise + */ + async getURL() { + let url = RUNNER_URL; + try { + await fetch(url); + } catch (e) { + url = null; + } + this.getURL = () => url; + return url; + }, + + async getTab(activate = false) { + let url = await this.getURL(); + let tab = url ? (await browser.tabs.query({url}))[0] || + (await browser.tabs.create({url})) + : null; + if (tab && activate) { + await browser.tabs.update(tab.id, {active: true}); + } + return tab; + } + }; +})(); +if (typeof module === "object") { + module.exports = Test; +} diff --git a/content/externalLicenseChecker.js b/content/externalLicenseChecker.js index 181e3f9..516057e 100644 --- a/content/externalLicenseChecker.js +++ b/content/externalLicenseChecker.js @@ -79,7 +79,7 @@ async checkLicensedScript(m) { let {url, cache} = m; if (!licensedScripts) licensedScripts = await fetchLicenseInfo(cache); - return licensedScripts.get(url); + return licensedScripts.get(url) || licensedScripts.get(url.replace(/\?.*/, '')); } } diff --git a/html/common.css b/html/common.css index cf2c5d1..14931fa 100644 --- a/html/common.css +++ b/html/common.css @@ -3,6 +3,7 @@ html { margin:0px; color:#000 !important; background:url('background-panel.png') !important; + font-family: sans-serif; } body { padding:0; @@ -26,4 +27,15 @@ h1.libre { font-weight:bold; background:url('librejs-title.png') no-repeat top left; text-indent:-1000px; + position: relative; +} +h1.libre span { + font-family: sans-serif; + position: absolute; + bottom: 32px; + padding: 0 16px; + left: 230px; + display: block; + text-indent: 0; + vertical-align: bottom; } diff --git a/html/display_panel/content/display-panel.html b/html/display_panel/content/display-panel.html index df153b3..80f4cb9 100644 --- a/html/display_panel/content/display-panel.html +++ b/html/display_panel/content/display-panel.html @@ -53,6 +53,7 @@ <button id="complain">Complain to site owner</button> <button id="report-tab">Show this report in a new tab</button> <button id="open-options">Settings...</button> + <button id="autotest">Automated self test...</button> </div> </div> <div id="info"> @@ -100,5 +101,6 @@ </div> </div> </body> +<script src="/common/Test.js"></script> <script src="main_panel.js"></script> </html> diff --git a/html/display_panel/content/main_panel.js b/html/display_panel/content/main_panel.js index c55b167..2509545 100644 --- a/html/display_panel/content/main_panel.js +++ b/html/display_panel/content/main_panel.js @@ -38,6 +38,18 @@ myPort.postMessage({"update": true, tabId: parseInt(currentReport && currentRepo // Display the actual extension version Number document.querySelector("#version").textContent = browser.runtime.getManifest().version; +// Enable autotest button if this is a test-enabled build / session +(async () => { + if (await Test.getURL()) { + let button = document.querySelector("#autotest"); + button.style.display = "block"; + button.onclick = async () => { + await Test.getTab(true); + close(); + } + } +})(); + var liTemplate = document.querySelector("#li-template"); liTemplate.remove(); diff --git a/html/display_panel/content/panel-styles.css b/html/display_panel/content/panel-styles.css index 0d849e4..940a8ff 100644 --- a/html/display_panel/content/panel-styles.css +++ b/html/display_panel/content/panel-styles.css @@ -141,7 +141,8 @@ span.blocked { display: none; } -.tab #must-reload, .tab #buttons, .empty #buttons { +.tab #must-reload, .tab #buttons, +.empty #complain, .empty #report-tab, #autotest { display: none; } diff --git a/main_background.js b/main_background.js index a145cc5..5c2e8df 100644 --- a/main_background.js +++ b/main_background.js @@ -599,6 +599,8 @@ function full_evaluate(script){ * This can only determine if a script is bad, not if it's good * * If it passes the intitial pass, it runs the full pass and returns the result + +* It returns an array of [flag (boolean, false if "bad"), reason (string, human readable report)] * */ function evaluate(script,name){ @@ -636,27 +638,24 @@ function evaluate(script,name){ return [flag,reason]; } - var final = full_evaluate(script); -// final[1] = final[1] + "<br>"; - return final; + return full_evaluate(script); } -function license_valid(matches){ - if(matches.length != 4){ - return [false, "malformed or unrecognized license tag"]; - } - if(matches[1] != "@license"){ - return [false, "malformed or unrecognized license tag"]; +function validateLicense(matches) { + if (!(Array.isArray(matches) && matches.length === 4)){ + return [false, "Malformed or unrecognized license tag."]; } - if(licenses[matches[3]] === undefined){ - return [false, "malformed or unrecognized license tag"]; + let [all, tag, link, id] = matches; + let license = licenses[id]; + if(!license){ + return [false, `Unrecognized license "${id}"`]; } - if(licenses[matches[3]]["Magnet link"] != matches[2]){ - return [false, "malformed or unrecognized license tag"]; + if(license["Magnet link"] != link){ + return [false, `License magnet link does not match for "${id}".`]; } - return [true,"Recognized license as '"+matches[3]+"'<br>"]; + return [true, `Recognized license: "${id}".`]; } /** * @@ -669,91 +668,89 @@ function license_valid(matches){ * reason text * ] */ -function license_read(script_src, name, external = false){ - - var reason_text = ""; - - var edited_src = ""; - var unedited_src = script_src; - var nontrivial_status; - var parts_denied = false; - var parts_accepted = false; - var license = legacy_license_lib.check(script_src); - if(license != false){ - return [true,script_src,"Licensed under: "+license]; +function license_read(scriptSrc, name, external = false){ + + let license = legacy_license_lib.check(scriptSrc); + if (license){ + return [true, scriptSrc, `Licensed under: ${license}`]; } - if (listManager.builtInHashes.has(hash(script_src))){ - return [true,script_src,"Common script known to be free software."]; + if (listManager.builtInHashes.has(hash(scriptSrc))){ + return [true, scriptSrc, "Common script known to be free software."]; } - while(true){ // TODO: refactor me - // TODO: support multiline comments - var matches = /\/[\/\*]\s*?(@license)\s([\S]+)\s([\S]+$)/gm.exec(unedited_src); - var empty = /[^\s]/gm.exec(unedited_src); - if(empty == null){ - return [true,edited_src,reason_text]; + + let editedSrc = ""; + let uneditedSrc = scriptSrc.trim(); + let reason = uneditedSrc ? "" : "Empty source."; + let partsDenied = false; + let partsAccepted = false; + + function checkTriviality(s) { + if (!s.trim()) { + return true; // empty, ignore it } - if(matches == null){ - if (external) - return [false,edited_src,"External script with no known license."]; - else - nontrivial_status = evaluate(unedited_src,name); - if(nontrivial_status[0] == true){ - parts_accepted = true; - edited_src += unedited_src; - } else{ - parts_denied = true; - edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; - } - reason_text += "\n" + nontrivial_status[1]; + let [trivial, message] = external ? + [false, "External script with no known license"] + : evaluate(s, name); + if (trivial) { + partsAccepted = true; + editedSrc += s; + } else { + partsDenied = true; + editedSrc += `\n/*\nLIBREJS BLOCKED: ${message}\n*/\n`; + } + reason += `\n${message}`; + return trivial; + } - if(parts_denied == true && parts_accepted == true){ - reason_text = "Script was determined partly non-trivial after editing. (check source for details)\n"+reason_text; - } - if(parts_denied == true && parts_accepted == false){ - return [false,edited_src,reason_text]; - } - else return [true,edited_src,reason_text]; + while (uneditedSrc) { + let openingMatch = /\/[\/\*]\s*?(@license)\s+(\S+)\s+(\S+)\s*$/mi.exec(uneditedSrc); + if (!openingMatch) { // no license found, check for triviality + checkTriviality(uneditedSrc); + break; + } + let openingIndex = openingMatch.index; + if (openingIndex) { + // let's check the triviality of the code before the license tag, if any + checkTriviality(uneditedSrc.substring(0, openingIndex)); } - // sponge - dbg_print("undedited_src:"); - dbg_print(unedited_src); - dbg_print(matches); - dbg_print("chopping at " + matches["index"] + "."); - var before = unedited_src.substring(0,matches["index"]); - // sponge - dbg_print("before:"); - dbg_print(before); - if (external) - nontrivial_status = [true, "External script with no known license"] - else - nontrivial_status = evaluate(before,name); - if(nontrivial_status[0] == true){ - parts_accepted = true; - edited_src += before; - } else{ - parts_denied = true; - edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; + // let's check the actual license + uneditedSrc = uneditedSrc.substring(openingIndex); + + let closureMatch = /\/([*/])\s*@license-end\b[^*/\n]*/i.exec(uneditedSrc); + if (!closureMatch) { + let msg = "ERROR: @license with no @license-end"; + return [false, `\n/*\n ${msg} \n*/\n`, msg]; } - unedited_src = unedited_src.substr(matches["index"],unedited_src.length); - // TODO: support multiline comments - var matches_end = /\/\/\s*?(@license-end)/gm.exec(unedited_src); - if(matches_end == null){ - dbg_print("ERROR: @license with no @license-end"); - return [false,"\n/*\n ERROR: @license with no @license-end \n*/\n","ERROR: @license with no @license-end"]; + + let closureEndIndex = closureMatch.index + closureMatch[0].length; + let commentEndOffset = uneditedSrc.substring(closureEndIndex).indexOf(closureMatch[1] === "*" ? "*/" : "\n"); + if (commentEndOffset !== -1) { + closureEndIndex += commentEndOffset; } - var endtag_end_index = matches_end["index"]+matches_end[0].length; - var license_res = license_valid(matches); - if(license_res[0] == true){ - edited_src = edited_src + unedited_src.substr(0,endtag_end_index); - reason_text += "\n" + license_res[1]; - } else{ - edited_src = edited_src + "\n/*\n"+license_res[1]+"\n*/\n"; - reason_text += "\n" + license_res[1]; + + let [licenseOK, message] = validateLicense(openingMatch); + if(licenseOK) { + editedSrc += uneditedSrc.substr(0, closureEndIndex); + partsAccepted = true; + } else { + editedSrc += `\n/*\n${message}\n*/\n`; + partsDenied = true; } + reason += `\n${message}`; + // trim off everything we just evaluated - unedited_src = unedited_src.substr(endtag_end_index,unedited_src.length); + uneditedSrc = uneditedSrc.substring(closureEndIndex).trim(); + } + + if(partsDenied) { + if (partsAccepted) { + reason = `Some parts of the script have been disabled (check the source for details).\n^--- ${reason}`; + } + return [false, editedSrc, reason]; } + + return [true, scriptSrc, reason]; } /* *********************************************************************************************** */ @@ -862,6 +859,7 @@ var ResponseHandler = { let {request} = response; let {url, type, tabId, frameId, documentUrl} = request; + let fullUrl = url; url = ListStore.urlItem(url); let site = ListStore.siteItem(url); @@ -891,11 +889,11 @@ var ResponseHandler = { "whitelisted": [url, whitelistedSite ? `User whitelisted ${site}` : "Whitelisted by user"]}); return ResponseProcessor.ACCEPT; } else { - let scriptInfo = await ExternalLicenses.check({url, tabId, frameId, documentUrl}); + let scriptInfo = await ExternalLicenses.check({url: fullUrl, tabId, frameId, documentUrl}); if (scriptInfo) { let verdict, ret; let msg = scriptInfo.toString(); - if (scriptInfo.allFree) { + if (scriptInfo.free) { verdict = "accepted"; ret = ResponseProcessor.ACCEPT; } else { @@ -1177,6 +1175,21 @@ async function init_addon() { ResponseProcessor.install(ResponseHandler); legacy_license_lib.init(); + + + let Test = require("./common/Test"); + if (Test.getURL()) { + // export testable functions to the global scope + this.LibreJS = { + editHtml, + handle_script, + ExternalLicenses, + }; + // create or focus the autotest tab if it's a debugging session + if ((await browser.management.getSelf()).installType === "development") { + Test.getTab(true); + } + } } diff --git a/test/SpecRunner.html b/test/SpecRunner.html new file mode 100644 index 0000000..42a372f --- /dev/null +++ b/test/SpecRunner.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> +<!-- +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ +--> +<head> + <meta charset="utf-8"> + <title>LibreJS Tests</title> + + <link rel="shortcut icon" type="image/png" href="/icons/librejs.png"> + <link rel="stylesheet" href="lib/jasmine-3.2.1/jasmine.css"> + <link rel="stylesheet" href="/html/common.css"> + <script src="lib/jasmine-3.2.1/jasmine.js"></script> + <script src="lib/jasmine-3.2.1/jasmine-html.js"></script> + <script src="lib/jasmine-3.2.1/boot.js"></script> + + <script src="spec/LibreJSSpec.js"></script> + +</head> + +<body> + <h1 class="libre">LibreJS <span>Autotest</span></h1> +</body> +</html> diff --git a/test/resources/app-trilicensed.js b/test/resources/app-trilicensed.js new file mode 100644 index 0000000..7b2c2ea --- /dev/null +++ b/test/resources/app-trilicensed.js @@ -0,0 +1 @@ +document.write(`<p>Executing ${document.currentScript.src}</p>`); diff --git a/test/resources/index.html b/test/resources/index.html new file mode 100644 index 0000000..c5f364c --- /dev/null +++ b/test/resources/index.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<!-- +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ +--> +<head> + <meta charset="utf-8"> + <title>LibreJS test document</title> + <link rel="shortcut icon" type="image/png" href="/icons/librejs.png"> +</head> +<body> +<h1>LibreJS test document</h1> +<a href="jslicense.html" rel="jslicense">JavaScript license information</a> +<script src="jquery.js"></script> +<script src="app-trilicensed.js"></script> +<script src="proprietary.js"></script> +<script src="tracker.js"></script> +</body> +</html> diff --git a/test/resources/jquery.js b/test/resources/jquery.js new file mode 100644 index 0000000..7b2c2ea --- /dev/null +++ b/test/resources/jquery.js @@ -0,0 +1 @@ +document.write(`<p>Executing ${document.currentScript.src}</p>`); diff --git a/test/resources/jslicense.html b/test/resources/jslicense.html new file mode 100644 index 0000000..0cd0449 --- /dev/null +++ b/test/resources/jslicense.html @@ -0,0 +1,25 @@ +<table id="jslicense-labels1"> +<tr> +<td><a href="jquery.js">Fake jQuery</a></td> +<td><a href="http://www.jclark.com/xml/copying.txt">Expat</a> +<td><a href="jquery-1.7.tar.gz">jquery-1.7.tar.gz</a></td> +</tr> +<tr> +<td><a href="app-trilicensed.js">App specific script, tri-licensed (2 free and 1 proprietary license)</a></td> +<td> +<a href="magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt">GNU V3</a> +<br> +<a href="http://www.jclark.com/xml/copying.txt">Expat</a> +<br> +<a href="https://evil.com/someproprietarylicense">Proprietary, optional</a></td> +</td> +<td><a href="app-trilicensed.js">app-trilicensed.js</a></td> +</tr> +<tr> +<td><a href="proprietary.js">App specific script, proprietary only</a></td> +<td> +<a href="https://evil.com/someproprietarylicense">Proprietary</a></td> +</td> +<td><a href="proprietary.js">proprietary.js</a></td> +</tr> +</table> diff --git a/test/resources/proprietary.js b/test/resources/proprietary.js new file mode 100644 index 0000000..7b2c2ea --- /dev/null +++ b/test/resources/proprietary.js @@ -0,0 +1 @@ +document.write(`<p>Executing ${document.currentScript.src}</p>`); diff --git a/test/resources/tracker.js b/test/resources/tracker.js new file mode 100644 index 0000000..7b2c2ea --- /dev/null +++ b/test/resources/tracker.js @@ -0,0 +1 @@ +document.write(`<p>Executing ${document.currentScript.src}</p>`); diff --git a/test/spec/LibreJSSpec.js b/test/spec/LibreJSSpec.js new file mode 100644 index 0000000..3d61973 --- /dev/null +++ b/test/spec/LibreJSSpec.js @@ -0,0 +1,208 @@ +/* +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ +"use strict"; + +describe("LibreJS' components", () => { + let LibreJS = browser.extension.getBackgroundPage().LibreJS; + let license = { + id: 'GPL-3.0', + url: 'http://www.gnu.org/licenses/gpl-3.0.html', + magnet: 'magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt', + }; + let unknownLicense = { + id: 'Acme-proprietary-1.5', + url: 'http://www.acme.com/license-1.5.html', + magnet: 'magnet:?xt=urn:btih:2f739d935676111cfff4b4693e3816e664797050&dn=acme-1.5.txt' + }; + + let trivial = "1+1"; + let nontrivial = `function nt() { document.documentElement.innerHTML=""; nt(); }`; + let licensed = `// @license ${license.magnet} ${license.id}\n${nontrivial}\n// @license-end`; + let unknownLicensed = `// @license ${unknownLicense.magnet} ${unknownLicense.id}\n${nontrivial}\n// @license-end`; + let malformedLicensed = `// @license\n${nontrivial}`; + + let tab, documentUrl; + + beforeAll(async () => { + let url = browser.extension.getURL("/test/resources/index.html"); + tab = (await browser.tabs.query({url}))[0] || (await browser.tabs.create({url})); + documentUrl = url; + }); + + describe("The external script source processor", () => { + let url = "https://www.gnu.org/mock-script.js"; + + let processScript = async (source, whitelisted = false) => + await LibreJS.handle_script({ + text: source, + request: {url, tabId: tab.id, documentUrl, frameId: 0}, + }, whitelisted); + + it("should accept whitelisted scripts", async () => { + expect(await processScript(nontrivial, true) || nontrivial).toContain(nontrivial); + }); + + it("should block trivial scripts too", async () => { + let processed = await processScript(trivial); + expect(processed || trivial).not.toContain(trivial); + }); + + it("should block non-trivial scripts", async () => { + let processed = await processScript(nontrivial); + expect(processed || nontrivial).not.toContain(nontrivial); + }); + + it("should accept scripts with known free license tags", async () => { + let processed = await processScript(licensed); + expect(processed || licensed).toContain(nontrivial); + }); + + it("should block scripts with unknown license tags", async () => { + let processed = await processScript(unknownLicensed); + expect(processed).not.toContain(nontrivial); + }); + + it("should block scripts with malformed license tags", async () => { + let processed = await processScript(malformedLicensed); + expect(processed).not.toContain(nontrivial); + }); + }); + + describe("The HTML processor", () => { + let processHtml = + async (html, whitelisted = false) => + LibreJS.editHtml(html, tab.url, tab.id, 0, whitelisted); + + let addScript = (html, script, before = "</head>") => + html.replace(before, `<script>${script}</script>${before}`); + + function extractScripts(html, def = "") { + let matches = html && html.match(/<script>[^]*?<\/script>/g); + return matches && matches.join("") || def; + } + + let html, nontrivialInHtml; + beforeAll(async () => { + html = (await browser.tabs.executeScript(tab.id, { + runAt: "document_start", + code: "document.documentElement.outerHTML" + }))[0]; + nontrivialInHtml = addScript(html, nontrivial); + }); + + it("should not modify scriptless documents", async () => { + expect(await processHtml(html)).toBeNull(); + }); + + it("should not modify whitelisted documents", async () => { + expect(await processHtml(nontrivialInHtml, true)).toBeNull(); + }); + + it("should accept trivial scripts", async () => { + let trivialInHtml = addScript(html, trivial); + let processed = await processHtml(trivialInHtml); + expect(extractScripts(processed, trivial)).toContain(trivial); + }); + + it("should block non-trivial scripts", async () => { + let processed = await processHtml(nontrivialInHtml); + expect(extractScripts(processed, nontrivial)).not.toContain(nontrivial); + }); + + it("should accept scripts with known free license tags", async () => { + let licensedInHtml = addScript(html, licensed); + let processed = await processHtml(licensedInHtml); + expect(extractScripts(processed, licensed)).toContain(nontrivial); + }); + + it("should block scripts with unknown license tags", async () => { + let unknownInHtml = addScript(html, unknownLicensed); + let processed = await processHtml(unknownInHtml); + expect(extractScripts(processed, nontrivial)).not.toContain(nontrivial); + }); + + it("should block scripts with malformed license tags", async () => { + let malformedInHtml = addScript(html, malformedLicensed); + let processed = await processHtml(malformedInHtml); + expect(extractScripts(processed, nontrivial)).not.toContain(nontrivial); + }); + + it("should accept scripts on globally licensed pages", async () => { + let globalLicense = `/* @licstart The following is the entire license notice + for the JavaScript code in this page. + -- Some free license -- + @licend The above is the entire license notice for the JavaScript code in this page. */`; + + let licensed = addScript(nontrivialInHtml, globalLicense, "<script>"); + let processed = await processHtml(html); + expect(extractScripts(processed, licensed)).toContain(nontrivial); + }); + + it("should discriminate trivial, non-trivial and licensed mixed on the same page", async () => { + let mixedPage = addScript(addScript(nontrivialInHtml, trivial), licensed); + let processed = await processHtml(mixedPage); + expect(processed).not.toBeNull(); + let scripts = extractScripts(processed, nontrivial); + expect(scripts).toContain(trivial); + expect(scripts).toContain(licensed); + expect(scripts.replace(licensed, "")).not.toContain(nontrivial); + }); + }); + + describe("The external (Web Labels) license checker", () => { + let {ExternalLicenses} = LibreJS; + let check; + + beforeAll(async () => { + let args = {tabId: tab.id, frameId: 0, documentUrl}; + let resolve = url => new URL(url, documentUrl).href; + check = async url => await ExternalLicenses.check(Object.assign({url: resolve(url)}, args)); + await browser.tabs.executeScript(tab.id, { + file: "/content/externalLicenseChecker.js" + }); + }); + + it("should recognize free licenses", async () => { + let scriptInfo = await check("jquery.js"); + console.debug(scriptInfo); + expect(scriptInfo.free).toBeTruthy(); + }); + it("should accept scripts if any of multiple licenses is free", async () => { + let scriptInfo = await check("app-trilicensed.js"); + console.debug(scriptInfo); + expect(scriptInfo.free).toBeTruthy(); + }); + it("should block scripts declaring only proprietary license(s)", async () => { + let scriptInfo = await check("proprietary.js"); + console.debug(scriptInfo); + expect(scriptInfo.free).toBeFalsy(); + }); + it("should block scripts not declaring any license", async () => { + let scriptInfo = await check("tracker.js"); + console.debug(scriptInfo); + expect(scriptInfo).toBeNull(); + }); + }); + afterAll(async () => { + await browser.tabs.remove(tab.id); + browser.tabs.update((await browser.tabs.getCurrent()).id, {active: true}); + }); +}); |