diff --git a/.documentation/addon description/de/description.txt b/.documentation/addon description/de/description.txt
index d0b63b6..4a02c22 100644
--- a/.documentation/addon description/de/description.txt
+++ b/.documentation/addon description/de/description.txt
@@ -38,6 +38,7 @@ Beschützte "Fingerprinting"-APIs:
window (standardmäßig deaktiviert)
DOMRect
navigator (standardmäßig deaktiviert)
+ screen
Falls Sie Fehler finden oder Verbesserungsvorschläge haben, teilen Sie mir das bitte auf https://github.com/kkapsner/CanvasBlocker/issues mit.
\ No newline at end of file
diff --git a/.documentation/addon description/en/description.txt b/.documentation/addon description/en/description.txt
index 8a0ae78..6108243 100644
--- a/.documentation/addon description/en/description.txt
+++ b/.documentation/addon description/en/description.txt
@@ -39,6 +39,7 @@ Protected "fingerprinting" APIs:
window (disabled by default)
DOMRect
navigator (disabled by default)
+ screen
Please report issues and feature requests at https://github.com/kkapsner/CanvasBlocker/issues
diff --git a/README.md b/README.md
index 9420640..58333d8 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Protected "fingerprinting" APIs:
* window (disabled by default)
* DOMRect
* navigator (disabled by default)
+ * screen
Special thanks to:
* spodermenpls for finding all the typos
diff --git a/_locales/de/messages.json b/_locales/de/messages.json
index bce8160..d7d468a 100644
--- a/_locales/de/messages.json
+++ b/_locales/de/messages.json
@@ -147,6 +147,10 @@
"message": "Navigator-API",
"description": ""
},
+ "section_Screen-api": {
+ "message": "Screen-API",
+ "description": ""
+ },
"displayAdvancedSettings_title": {
"message": "Expertenmodus",
"description": ""
@@ -271,6 +275,18 @@
"message": "Wollen Sie das Auslesen über die Navigator-API erlauben?",
"description": ""
},
+ "askForScreenPermission": {
+ "message": "Wollen Sie die Screen-API erlauben?",
+ "description": ""
+ },
+ "askForScreenInputPermission": {
+ "message": "Wollen Sie das Schreiben über die Screen-API erlauben?",
+ "description": ""
+ },
+ "askForScreenReadoutPermission": {
+ "message": "Wollen Sie das Auslesen über die Screen-API erlauben?",
+ "description": ""
+ },
"askOnlyOnce_title": {
"message": "Nur einmal nachfragen",
"description": ""
@@ -655,6 +671,10 @@
"message": "Navigator-Auslese vorgetäuscht auf {url}",
"description": ""
},
+ "fakedScreenReadout": {
+ "message": "Screen-Auslese vorgetäuscht auf {url}",
+ "description": ""
+ },
"fakedInput": {
"message": "Bei Ausgabe vorgetäuscht auf {url}",
"description": ""
@@ -1123,6 +1143,34 @@
"message": "Zurücksetzen",
"description": ""
},
+ "protectScreen_title": {
+ "message": "Screen-API beschützen",
+ "description": ""
+ },
+ "protectScreen_description": {
+ "message": "Dies schützt vor Fingerprinting, das die Bildschirmgröße einbezieht.",
+ "description": ""
+ },
+ "protectScreen_urlSpecific": {
+ "message": "Um bestimmte Seiten von diesem Schutz auszuschließen, klicken Sie auf den schwarzen Pfeil um das Menü zu öffnen, fügen Sie die gewünschte Domain oder URL mit einem Klick auf \"+\" hinzu und entfernen Sie das zugehörige Häkchen.",
+ "description": ""
+ },
+ "screenSize_title": {
+ "message": "Bildschirmgröße",
+ "description": ""
+ },
+ "screenSize_description": {
+ "message": "Wenn dies auf einen Wert \"...x...\" gesetzt wird, werden diese Größen als Bildschirmgröße verwendet.",
+ "description": ""
+ },
+ "fakeMinimalScreenSize_title": {
+ "message": "Minimale Bildschirmgröße vortäuschen",
+ "description": ""
+ },
+ "fakeMinimalScreenSize_description": {
+ "message": "Verwende die minimale Bildschirmgröße aus der folgenden Liste, die zur inneren Browserfenstergröße passt. Bildschirmgrößen: 1366x768, 1440x900, 1600x900, 1920x1080, 4096x2160, 8192x4320",
+ "description": ""
+ },
"theme_title": {
"message": "Theme",
"description": ""
@@ -1407,6 +1455,10 @@
"message": "Teilen Sie die persistenten Zufallszahlen nicht zwischen Domains, da dies den Browser 100% eindeutig identifizierbar macht.",
"description": ""
},
+ "sanitation_error.customScreenSize": {
+ "message": "Verwenden Sie keine benutzerdefinierte Bildschirmgröße, da sie den Browser verfolgbarer macht.",
+ "description": ""
+ },
"whitelist_inspection_title": {
"message": "CanvasBlocker Erlaubnisse ansehen",
"description": ""
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 83d9caf..cb841f3 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -154,6 +154,10 @@
"message": "Navigator API",
"description": ""
},
+ "section_Screen-api":{
+ "message": "Screen API",
+ "description": ""
+ },
"displayAdvancedSettings_title": {
"message": "Expert mode",
@@ -283,6 +287,18 @@
"message": "Do you want to allow navigator API readout?",
"description": ""
},
+ "askForScreenPermission": {
+ "message": "Do you want to allow the screen API?",
+ "description": ""
+ },
+ "askForScreenInputPermission": {
+ "message": "Do you want to allow screen API input?",
+ "description": ""
+ },
+ "askForScreenReadoutPermission": {
+ "message": "Do you want to allow screen API readout?",
+ "description": ""
+ },
"askOnlyOnce_title": {
"message": "Ask only once",
"description": ""
@@ -689,6 +705,10 @@
"message": "Faked navigator readout on {url}",
"description": ""
},
+ "fakedScreenReadout": {
+ "message": "Faked screen readout on {url}",
+ "description": ""
+ },
"fakedInput": {
"message": "Faked at input on {url}",
"description": ""
@@ -1172,6 +1192,35 @@
"description": ""
},
+ "protectScreen_title": {
+ "message": "Protect screen API",
+ "description": ""
+ },
+ "protectScreen_description": {
+ "message": "This protects against fingerprinting attempts including the screen size.",
+ "description": ""
+ },
+ "protectScreen_urlSpecific": {
+ "message": "To exclude specific websites from this protection, click on the black arrow to open the menu, add the domain or URL by clicking on \"+\" and remove its checkmark.",
+ "description": ""
+ },
+ "screenSize_title": {
+ "message": "Screen size",
+ "description": ""
+ },
+ "screenSize_description": {
+ "message": "If this is set with a value \"...x...\" the specified dimensions will be reported as the screen size.",
+ "description": ""
+ },
+ "fakeMinimalScreenSize_title": {
+ "message": "Fake minimal screen size",
+ "description": ""
+ },
+ "fakeMinimalScreenSize_description": {
+ "message": "Use a minimal screen size from the following set that can fit the inner window dimensions. Screen sizes: 1366x768, 1440x900, 1600x900, 1920x1080, 4096x2160, 8192x4320",
+ "description": ""
+ },
+
"theme_title": {
"message": "Theme",
"description": ""
@@ -1466,6 +1515,10 @@
"message": "Do not share persistent randomness between domains because this makes the browser 100% trackable.",
"description": ""
},
+ "sanitation_error.customScreenSize": {
+ "message": "Do not use a custom screen size as it makes the browser more trackable.",
+ "description": ""
+ },
"whitelist_inspection_title": {
"message": "CanvasBlocker whitelist inspection",
diff --git a/lib/modifiedAPI.js b/lib/modifiedAPI.js
index 84a0d2f..329c46c 100644
--- a/lib/modifiedAPI.js
+++ b/lib/modifiedAPI.js
@@ -40,4 +40,5 @@
appendModified(require("./modifiedWindowAPI"));
appendModified(require("./modifiedDOMRectAPI"));
appendModified(require("./modifiedNavigatorAPI"));
+ appendModified(require("./modifiedScreenAPI"));
}());
\ No newline at end of file
diff --git a/lib/modifiedScreenAPI.js b/lib/modifiedScreenAPI.js
new file mode 100644
index 0000000..3b0fac2
--- /dev/null
+++ b/lib/modifiedScreenAPI.js
@@ -0,0 +1,338 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function(){
+ "use strict";
+
+ var scope;
+ if ((typeof exports) !== "undefined"){
+ scope = exports;
+ }
+ else {
+ scope = require.register("./modifiedScreenAPI", {});
+ }
+
+ const {checkerWrapper} = require("./modifiedAPIFunctions");
+
+ const physical = {
+ width: Math.round(window.screen.width * window.devicePixelRatio),
+ height: Math.round(window.screen.height * window.devicePixelRatio)
+ };
+ if (!window.matchMedia(`(device-width: ${physical.width / window.devicePixelRatio}px`).matches){
+ let minWidth = Math.ceil((window.screen.width - 0.5) * window.devicePixelRatio);
+ let maxWidth = Math.floor((window.screen.width + 0.5) * window.devicePixelRatio);
+ for (let width = minWidth; width <= maxWidth; width += 1){
+ if (window.matchMedia(`(device-width: ${width / window.devicePixelRatio}px`).matches){
+ physical.width = width;
+ break;
+ }
+ }
+ }
+ if (!window.matchMedia(`(device-height: ${physical.height / window.devicePixelRatio}px`).matches){
+ let minHeight = Math.ceil((window.screen.height - 0.5) * window.devicePixelRatio);
+ let maxHeight = Math.floor((window.screen.height + 0.5) * window.devicePixelRatio);
+ for (let height = minHeight; height <= maxHeight; height += 1){
+ if (window.matchMedia(`(device-height: ${height / window.devicePixelRatio}px`).matches){
+ physical.height = height;
+ break;
+ }
+ }
+ }
+
+ const resolutions = {
+ portrait: [
+ {height: 1366, width: 768},
+ {height: 1440, width: 900},
+ {height: 1600, width: 900},
+ {height: 1920, width: 1080},
+ {height: 4096, width: 2160},
+ {height: 8192, width: 4320},
+ ],
+ landscape: [
+ {width: 1366, height: 768},
+ {width: 1440, height: 900},
+ {width: 1600, height: 900},
+ {width: 1920, height: 1080},
+ {width: 4096, height: 2160},
+ {width: 8192, height: 4320},
+ ]
+ };
+
+ function getScreenDimensions(prefs, window){
+ const screenSize = prefs("screenSize", window.location);
+ if (screenSize.match(/\s*\d+\s*x\s*\d+\s*$/)){
+ const [width, height] = screenSize.split("x").map(function(value){
+ return Math.round(parseFloat(value.trim()));
+ });
+ return {
+ width: width / window.devicePixelRatio,
+ height: height / window.devicePixelRatio
+ };
+ }
+ if (!prefs("fakeMinimalScreenSize", window.location)){
+ return window.screen;
+ }
+ const isLandscape = window.screen.width > window.screen.height;
+ // subtract 0.5 to adjust for potential rounding errors
+ const innerWidth = (window.innerWidth - 0.5) * window.devicePixelRatio;
+ const innerHeight = (window.innerHeight - 0.5) * window.devicePixelRatio;
+ for (let resolution of resolutions[isLandscape? "landscape": "portrait"]){
+ if (resolution.width >= innerWidth && resolution.height >= innerHeight){
+ return {
+ width: resolution.width / window.devicePixelRatio,
+ height: resolution.height / window.devicePixelRatio
+ };
+ }
+ }
+ return window.screen;
+ }
+
+ scope.changedGetters = [
+ {
+ objectGetters: [function(window){return window.Screen && window.Screen.prototype;}],
+ name: "width",
+ getterGenerator: function(checker){
+ const temp = {
+ get width(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const returnValue = Math.round(getScreenDimensions(prefs, window).width);
+ if (originalValue !== returnValue){
+ notify("fakedScreenReadout");
+ }
+ return returnValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "width").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window.Screen && window.Screen.prototype;}],
+ name: "height",
+ getterGenerator: function(checker){
+ const temp = {
+ get height(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const returnValue = Math.round(getScreenDimensions(prefs, window).height);
+ if (originalValue !== returnValue){
+ notify("fakedScreenReadout");
+ }
+ return returnValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "height").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window.Screen && window.Screen.prototype;}],
+ name: "availWidth",
+ getterGenerator: function(checker){
+ const temp = {
+ get availWidth(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const returnValue = Math.round(getScreenDimensions(prefs, window).width);
+ if (originalValue !== returnValue){
+ notify("fakedScreenReadout");
+ }
+ return returnValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "availWidth").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window.Screen && window.Screen.prototype;}],
+ name: "availHeight",
+ getterGenerator: function(checker){
+ const temp = {
+ get availHeight(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const returnValue = Math.round(getScreenDimensions(prefs, window).height);
+ if (originalValue !== returnValue){
+ notify("fakedScreenReadout");
+ }
+ return returnValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "availHeight").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window.Screen && window.Screen.prototype;}],
+ name: "availLeft",
+ getterGenerator: function(checker){
+ const temp = {
+ get availLeft(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ if (originalValue !== 0){
+ notify("fakedScreenReadout");
+ }
+ return 0;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "availLeft").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window.Screen && window.Screen.prototype;}],
+ name: "availTop",
+ getterGenerator: function(checker){
+ const temp = {
+ get availTop(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ if (originalValue !== 0){
+ notify("fakedScreenReadout");
+ }
+ return 0;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "availTop").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window;}],
+ name: "outerWidth",
+ getterGenerator: function(checker){
+ const temp = {
+ get outerWidth(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const returnValue = window.innerWidth;
+ if (originalValue !== returnValue){
+ notify("fakedScreenReadout");
+ }
+ return returnValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "outerWidth").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window;}],
+ name: "outerHeight",
+ getterGenerator: function(checker){
+ const temp = {
+ get outerHeight(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const returnValue = window.innerHeight;
+ if (originalValue !== returnValue){
+ notify("fakedScreenReadout");
+ }
+ return returnValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "outerHeight").get;
+ }
+ },
+ {
+ objectGetters: [function(window){return window.MediaQueryList && window.MediaQueryList.prototype;}],
+ name: "matches",
+ getterGenerator: function(checker){
+ const temp = {
+ get matches(){
+ return checkerWrapper(checker, this, arguments, function(args, check){
+ const {prefs, notify, window, original} = check;
+ const originalValue = original.apply(this, window.Array.from(args));
+ const screenSize = prefs("screenSize", window.location);
+ if (
+ (
+ screenSize.match(/\s*\d+\s*x\s*\d+\s*$/) ||
+ prefs("fakeMinimalScreenSize", window.location)
+ ) &&
+ this.media.match(/device-(width|height)/)
+ ){
+ const dimensions = getScreenDimensions(prefs, window);
+ const originalMedia = this.media;
+ const alteredMedia = this.media.replace(
+ /\(\s*(?:(min|max)-)?device-(width|height):\s+(\d+\.?\d*)px\s*\)/,
+ function(m, type, dimension, value){
+ value = parseFloat(value);
+ let newCompareValue = value;
+ switch (type){
+ case "min":
+ if (value <= dimensions[dimension]){
+ newCompareValue = 0;
+ }
+ else {
+ newCompareValue = 2 * physical[dimension];
+ }
+ break;
+ case "max":
+ if (value >= dimensions[dimension]){
+ newCompareValue = 2 * physical[dimension];
+ }
+ else {
+ newCompareValue = 0;
+ }
+ break;
+ default:
+ if (
+ Math.round(value * 100) ===
+ Math.round(dimensions[dimension] * 100)
+ ){
+ newCompareValue = physical[dimension];
+ }
+ else {
+ newCompareValue = 0;
+ }
+ }
+ return "(" + (type? type + "-": "") +
+ "device-" + dimension + ": " +
+ (
+ newCompareValue /
+ window.devicePixelRatio
+ ) + "px)";
+ }
+ );
+ if (alteredMedia !== originalMedia){
+ const alteredQuery = window.matchMedia(alteredMedia);
+ const fakedValue = original.call(alteredQuery);
+ if (originalValue !== fakedValue){
+ notify("fakedScreenReadout");
+ }
+ return fakedValue;
+ }
+ }
+ return originalValue;
+ });
+ }
+ };
+ return Object.getOwnPropertyDescriptor(temp, "matches").get;
+ }
+ },
+ ];
+
+ function getStatus(obj, status, prefs){
+ status = Object.create(status);
+ status.active = prefs("protectScreen", status.url);
+ return status;
+ }
+
+ scope.changedGetters.forEach(function(changedGetter){
+ changedGetter.type = "readout";
+ changedGetter.getStatus = getStatus;
+ changedGetter.api = "screen";
+ });
+}());
\ No newline at end of file
diff --git a/lib/settingDefinitions.js b/lib/settingDefinitions.js
index cea546c..2637de6 100644
--- a/lib/settingDefinitions.js
+++ b/lib/settingDefinitions.js
@@ -130,6 +130,16 @@
"userAgent @ navigator",
"vendor @ navigator",
"vendorSub @ navigator",
+ {name: "Screen-API", level: 1},
+ "width @ screen",
+ "height @ screen",
+ "availWidth @ screen",
+ "availHeight @ screen",
+ "availTop @ screen",
+ "availLeft @ screen",
+ "matches @ screen",
+ "outerWidth @ screen",
+ "outerHeight @ screen",
],
defaultKeyValue: true
},
@@ -334,6 +344,21 @@
name: "navigatorDetails",
defaultValue: {},
},
+ {
+ name: "protectScreen",
+ defaultValue: true,
+ urlSpecific: true
+ },
+ {
+ name: "screenSize",
+ defaultValue: "",
+ urlSpecific: true
+ },
+ {
+ name: "fakeMinimalScreenSize",
+ defaultValue: true,
+ urlSpecific: true
+ },
{
name: "displayAdvancedSettings",
defaultValue: false
diff --git a/manifest.json b/manifest.json
index 607d1f0..c7cffdf 100644
--- a/manifest.json
+++ b/manifest.json
@@ -51,6 +51,7 @@
"lib/modifiedDOMRectAPI.js",
"lib/navigator.js",
"lib/modifiedNavigatorAPI.js",
+ "lib/modifiedScreenAPI.js",
"lib/modifiedAPI.js",
"lib/randomSupplies.js",
"lib/intercept.js",
diff --git a/options/presets.json b/options/presets.json
index db640c5..1fcc041 100644
--- a/options/presets.json
+++ b/options/presets.json
@@ -11,7 +11,8 @@
"rng": "persistent",
"ignoreFrequentColors": 3,
"minColors": 3,
- "storePersistentRnd": true
+ "storePersistentRnd": true,
+ "fakeMinimalScreenSize": false
},
"max_protection": {
"minFakeSize": 0,
diff --git a/options/sanitationRules.js b/options/sanitationRules.js
index 3d0b275..c05da90 100644
--- a/options/sanitationRules.js
+++ b/options/sanitationRules.js
@@ -81,6 +81,7 @@
{mainFlag: "protectWindow", section: "Window-API"},
{mainFlag: "protectDOMRect", section: "DOMRect-API"},
{mainFlag: "protectNavigator", section: "Navigator-API"},
+ {mainFlag: "protectScreen", section: "Screen-API"},
].forEach(function(api){
if (settings.get(api.mainFlag) !== (api.mainFlagDisabledValue || false)){
let inSection = false;
@@ -297,6 +298,19 @@
}]
});
}
+ if (settings.protectScreen && settings.screenSize){
+ errorCallback({
+ message: extension.getTranslation("sanitation_error.customScreenSize"),
+ severity: "medium",
+ resolutions: [{
+ label: extension.getTranslation("sanitation_resolution.setTo")
+ .replace(/{value}/g, "\"\""),
+ callback: function(){
+ settings.screenSize = "";
+ }
+ }]
+ });
+ }
}
},
];
diff --git a/options/settingsDisplay.js b/options/settingsDisplay.js
index ac68735..8b47c41 100644
--- a/options/settingsDisplay.js
+++ b/options/settingsDisplay.js
@@ -632,6 +632,44 @@
},
]
},
+ {
+ name: "Screen-API",
+ settings: [
+ {
+ "name": "protectScreen"
+ },
+ {
+ "name": "protectedAPIFeatures",
+ "replaceKeyPattern": / @ .+$/,
+ "displayedSection": "Screen-API",
+ "displayDependencies": [
+ {
+ "protectScreen": [true],
+ "displayAdvancedSettings": [true]
+ }
+ ]
+ },
+ {
+ "name": "screenSize",
+ "displayDependencies": [
+ {
+ "protectScreen": [true],
+ "fakeMinimalScreenSize": [false],
+ "displayAdvancedSettings": [true]
+ }
+ ]
+ },
+ {
+ "name": "fakeMinimalScreenSize",
+ "displayDependencies": [
+ {
+ "protectScreen": [true],
+ "screenSize": [""]
+ }
+ ]
+ },
+ ]
+ },
]
},
{
diff --git a/releaseNotes.txt b/releaseNotes.txt
index 13414ff..a75bc42 100644
--- a/releaseNotes.txt
+++ b/releaseNotes.txt
@@ -3,7 +3,7 @@ Version 0.5.15:
- improved storage of protected API features
new features:
- -
+ - added screen protection
fixes:
- background color of the textarea in the settings export was not readable in the dark theme when the value was invalid
diff --git a/versions/updates.json b/versions/updates.json
index 4542e66..2d65f28 100644
--- a/versions/updates.json
+++ b/versions/updates.json
@@ -77,6 +77,10 @@
{
"version": "0.5.15Alpha20190924",
"update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-0.5.15Alpha20190924-an+fx.xpi"
+ },
+ {
+ "version": "0.5.15Alpha20191111",
+ "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-0.5.15Alpha20191111-an+fx.xpi"
}
]
}