From df1348420bdcfc2d089e77ea0303dbd55144c407 Mon Sep 17 00:00:00 2001 From: hackademix Date: Mon, 20 Aug 2018 14:18:15 +0200 Subject: WebLabels-based license checking implementation. --- bg/ExternalLicenses.js | 96 +++++++++++++++++++++++++++++++++++++++ content/externalLicenseChecker.js | 87 +++++++++++++++++++++++++++++++++++ main_background.js | 27 +++++++++-- manifest.json | 14 +++++- 4 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 bg/ExternalLicenses.js create mode 100644 content/externalLicenseChecker.js diff --git a/bg/ExternalLicenses.js b/bg/ExternalLicenses.js new file mode 100644 index 0000000..74692d7 --- /dev/null +++ b/bg/ExternalLicenses.js @@ -0,0 +1,96 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone +* +* 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 . +*/ + +/** + Singleton to handle external licenses, e.g. WebLabels +*/ + +"use strict"; + +let licensesByURL = new Map(); +{ + let {licenses} = require("../license_definitions"); + for (let l of Object.values(licenses).filter(l => l.canonicalUrl)) { + for (let url of l.canonicalUrl) { + licensesByURL.set(url, l); + } + } +} + +var ExternalLicenses = { + async check(script) { + let {url, tabId, frameId} = script; + let scriptInfo = await browser.tabs.sendMessage(tabId, { + action: "checkLicensedScript", + url + }, {frameId}); + if (!(scriptInfo && scriptInfo.licenseURLs.length)) { + return null; + } + scriptInfo.licenses = new Set(); + scriptInfo.allFree = true; + scriptInfo.toString = function() { + let licenseIds = [...this.licenses].map(l => l.identifier).sort().join(", "); + return this.allFree ? `Free license${licenseIds.length > 1 ? "s" : ""} (${licenseIds})` : `Mixed free (${licenseIds}) and unknown licenses`; + } + + for (let u of scriptInfo.licenseURLs) { + if (licensesByURL.has(u)) { + scriptInfo.licenses.add(licensesByURL.get(u)); + } else { + scriptInfo.allFree = false; + break; + } + } + return scriptInfo; + }, + + /** + * moves / creates external license references before any script in the page + * if needed, to have them ready when the first script load is triggered + * Returns true if the document has been actually modified, false otherwise. + */ + optimizeDocument(document) { + let link = document.querySelector(`link[rel="jslicense"], link[data-jslicense="1"], a[rel="jslicense"], a[data-jslicense="1"]`); + if (link) { + let move = () => !!document.head.insertBefore(link, document.head.firstChild); + if (link.parentNode === document.head) { + for (let node; node = link.previousElementSibling;) { + if (node.tagName.toUpperCase() === "SCRIPT") { + return move(); + } + } + } else { // the reference is only in the body + if (link.tagName.toUpperCase() === "A") { + let newLink = document.createElement("link"); + newLink.rel = "jslicense"; + newLink.setAttribute("href", link.getAttribute("href")); + link = newLink; + } + return move(); + } + } + return false; + } +}; + + +module.exports = { ExternalLicenses }; diff --git a/content/externalLicenseChecker.js b/content/externalLicenseChecker.js new file mode 100644 index 0000000..89f871f --- /dev/null +++ b/content/externalLicenseChecker.js @@ -0,0 +1,87 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone +* +* 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 . +*/ +"use strict"; +{ + let licensedScripts = null; + + let fetchWebLabels = async (map = new Map()) => { + // see https://www.gnu.org/software/librejs/free-your-javascript.html#step3 + + let link = document.querySelector(`link[rel="jslicense"], link[data-jslicense="1"], a[rel="jslicense"], a[data-jslicense="1"]`); + if (link) try { + let baseURL = link.href; + let response = await fetch(baseURL); + if (!response.ok) throw `${response.status} ${response.statusText}`; + let doc = new DOMParser().parseFromString( + await response.text(), + "text/html" + ); + let base = doc.querySelector("base"); + if (base) { + base.href = base.href; + } else { + doc.head.appendChild(doc.createElement("base")).href = baseURL; + } + let firstURL = parent => parent.querySelector("a").href; + let allURLs = parent => Array.map(parent.querySelectorAll("a"), a => a.href); + for (let row of doc.querySelectorAll("table#jslicense-labels1 tr")) { + let cols = row.querySelectorAll("td"); + let scriptURL = firstURL(cols[0]); + let licenseURLs = allURLs(cols[1]); + let sourceURLs = cols[2] ? allURLs(cols[2]) : []; + map.set(scriptURL, {scriptURL, licenseURLs, sourceURLs}); + } + } catch (e) { + console.error("Error fetching Web Labels at %o", link, e); + } + return map; + } + + let fetchLicenseInfo = async () => { + let map = new Map(); + + // in the fetchXxx methods we add to a map whatever license(s) + // URLs and source code references we can find in various formats + // (WebLabels is currently the only implementation), keyed by script URLs. + await Promise.all([ + fetchWebLabels(map), + // fetchXmlSpdx(), + // fetchTxtSpdx(), + // ... + ]); + return map; + } + + let handlers = { + async checkLicensedScript(m) { + let {url} = m; + if (!licensedScripts) licensedScripts = await fetchLicenseInfo(); + return licensedScripts.get(url); + } + } + + browser.runtime.onMessage.addListener(async m => { + if (m.action in handlers) { + debug("Received message", m); + return await handlers[m.action](m); + } + }); +} diff --git a/main_background.js b/main_background.js index 799c69f..1235e4a 100644 --- a/main_background.js +++ b/main_background.js @@ -28,6 +28,7 @@ var legacy_license_lib = require("./legacy_license_check.js"); var {ResponseProcessor} = require("./bg/ResponseProcessor"); var {Storage, ListStore} = require("./bg/Storage"); var {ListManager} = require("./bg/ListManager"); +var {ExternalLicenses} = require("./bg/ExternalLicenses"); console.log("main_background.js"); /** @@ -263,7 +264,7 @@ function updateReport(tabId, oldReport, updateUI = false){ * Make sure it will use the right URL when refering to a certain script. * */ -async function addReportEntry(tabId, scriptHashOrUrl, action, update = false) { +async function addReportEntry(tabId, scriptHashOrUrl, action) { let report = activityReports[tabId]; if (!report) report = activityReports[tabId] = createReport({url: (await browser.tabs.get(tabId)).url}); @@ -312,7 +313,7 @@ async function addReportEntry(tabId, scriptHashOrUrl, action, update = false) { } browser.sessions.setTabValue(tabId, report.url, report); - + updateBadge(tabId, report); return entryType; } @@ -903,8 +904,23 @@ var ResponseHandler = { */ async function handle_script(response, whitelisted){ let {text, request} = response; - let {url, tabId} = request; + let {url, tabId, frameId} = request; url = ListStore.urlItem(url); + if (!whitelisted) { + let scriptInfo = await ExternalLicenses.check({url, tabId, frameId}); + if (scriptInfo) { + let verdict; + let msg = scriptInfo.toString(); + if (scriptInfo.allFree) { + verdict = "accepted"; + } else { + verdict = "blocked"; + text = `/* ${msg} */`; + } + addReportEntry(tabId, url, {url, [verdict]: [url, msg]}); + return text; + } + } let edited = await get_script(text, url, tabId, whitelisted, -2); return Array.isArray(edited) ? edited[0] : edited; } @@ -985,7 +1001,10 @@ function edit_html(html,url,tabid,wl){ var parser = new DOMParser(); var html_doc = parser.parseFromString(html, "text/html"); - + + // moves external licenses reference, if any, before any