summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.sh14
-rw-r--r--tails-download-and-verify/.jpmignore4
-rw-r--r--tails-download-and-verify/bootstrap.js11
-rw-r--r--tails-download-and-verify/conf.json20
-rw-r--r--tails-download-and-verify/conf.json.sample39
-rw-r--r--tails-download-and-verify/data/download-page.js38
-rw-r--r--tails-download-and-verify/index.js3
-rw-r--r--tails-download-and-verify/install.rdf26
-rw-r--r--tails-download-and-verify/lib/cert-pinner.js46
-rw-r--r--tails-download-and-verify/lib/downloader.js12
-rw-r--r--tails-download-and-verify/lib/mirror-dispatcher.js232
-rw-r--r--tails-download-and-verify/package.json2
-rw-r--r--www/dave.xpibin19043 -> 23412 bytes
-rw-r--r--www/download.html2
14 files changed, 401 insertions, 48 deletions
diff --git a/build.sh b/build.sh
index 13ab33e..35dc938 100644
--- a/build.sh
+++ b/build.sh
@@ -2,6 +2,7 @@
base=$(dirname "$0")
dir="$base/tails-download-and-verify"
mirror_dispatcher_url='https://git-tails.immerda.ch/mirror-pool-dispatcher/plain/lib/js/mirror-dispatcher.js'
+mirror_dispatcher_file="lib/mirror-dispatcher.js"
pushd "$dir" >/dev/null 2>&1 || (
echo >&2 "FATAL: Channot chdir to $dir."
exit 1
@@ -9,12 +10,17 @@ pushd "$dir" >/dev/null 2>&1 || (
echo -n "Removing old XPIs... "
rm -f *.xpi >/dev/null && echo "done."
ver=$(egrep '"version": "[0-9]+\.[^"]+"' package.json | sed -re 's/.*"([0-9]+\.[^"]+)".*/\1/')
-echo "Importing mirror-dispatcher.js library"
-curl \
+
+if [ -f "$mirror_dispatcher_file" ]; then
+ echo "$mirror_dispatcher_file exists, won't download it (erase the local file to update from the $mirror_dispatcher_url)."
+else
+ echo "Importing $mirror_dispatcher_file from $mirror_dispatcher_url ..."
+ curl \
--proto -all,https \
--tlsv1 \
- --output "lib/mirror-dispatcher.js" \
+ --output "$mirror_dispatcher_file" \
"$mirror_dispatcher_url"
+fi
echo "Building extension version $ver"
jpm xpi || exit 2
popd
@@ -22,7 +28,7 @@ echo "Copying "*.xpi" to $base/www/dave.xpi..."
cp "$dir/"*.xpi "$base/www/dave.xpi" >/dev/null
ver_page="$base/www/download.html"
ver_line=$(egrep '<[a-z][^>]+id="extension-version"[^>]*>[0-9]' $ver_page)
-if [ "$ver_line" ] && ! echo "$ver_line" | fgrep "$ver" ; then
+if [ "$ver_line" ] && ! echo "$ver_line" | grep -F "$ver" ; then
echo "Updating download.html to version $ver..."
sed -re 's/(<[a-z][^>]+id="extension-version"[^>]*>)[0-9\.rcba]+/\1'$ver'/' $ver_page > $ver_page.upd && mv $ver_page.upd $ver_page
fi
diff --git a/tails-download-and-verify/.jpmignore b/tails-download-and-verify/.jpmignore
new file mode 100644
index 0000000..d0c3bb8
--- /dev/null
+++ b/tails-download-and-verify/.jpmignore
@@ -0,0 +1,4 @@
+*.sample
+*.zip
+*.xpi
+.*
diff --git a/tails-download-and-verify/bootstrap.js b/tails-download-and-verify/bootstrap.js
new file mode 100644
index 0000000..4866564
--- /dev/null
+++ b/tails-download-and-verify/bootstrap.js
@@ -0,0 +1,11 @@
+/* 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/. */
+"use strict";
+
+const { utils: Cu } = Components;
+const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", "");
+const COMMONJS_URI = "resource://gre/modules/commonjs";
+const { require } = Cu.import(COMMONJS_URI + "/toolkit/require.js", {});
+const { Bootstrap } = require(COMMONJS_URI + "/sdk/addon/bootstrap.js");
+const { startup, shutdown, install, uninstall } = new Bootstrap(rootURI);
diff --git a/tails-download-and-verify/conf.json b/tails-download-and-verify/conf.json
index 59afc60..a62f785 100644
--- a/tails-download-and-verify/conf.json
+++ b/tails-download-and-verify/conf.json
@@ -6,24 +6,24 @@
"pins": {
"domains": {
"tails.boum.org": {
- "cert": null,
- "issuer": "Gandi"
+ "certs": null,
+ "issuers": ["Let's Encrypt", "Gandi"]
}
},
"certs": {
- "*.boum.org": {
- "subjectName":"CN=*.boum.org,OU=Gandi Standard Wildcard SSL,OU=Domain Control Validated",
- "issuerOrganization":"Gandi",
- "sha256Fingerprint":"FB:89:1F:85:61:8D:6F:62:EA:A6:6E:92:4D:3A:FC:80:17:03:D6:FB:D5:F4:B0:31:E7:D7:5A:7F:55:06:74:2D",
- "serialNumber":"00:84:A7:E7:40:C4:D4:54:54:64:E4:35:22:38:F0:29:53"
- }
- },
- "issuers": {
"Gandi": {
"subjectName":"CN=Gandi Standard SSL CA 2,O=Gandi,L=Paris,ST=Paris,C=FR",
"issuerOrganization":"The USERTRUST Network",
"sha256Fingerprint":"B9:F2:16:43:23:63:8D:CE:0B:92:21:8B:43:C4:1C:1B:2B:26:96:38:93:29:DB:19:F5:CF:7A:D4:9B:5C:B3:72",
"serialNumber":"05:E4:DC:3B:94:38:AB:3B:85:97:CB:A6:A1:98:50:E3"
+ },
+ "Let's Encrypt": {
+ "subjectName":"CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US",
+ "commonName":"Let's Encrypt Authority X3",
+ "issuerOrganization":"Digital Signature Trust Co.",
+ "organization":"Let's Encrypt",
+ "sha256Fingerprint":"25:84:7D:66:8E:B4:F0:4F:DD:40:B1:2B:6B:07:40:C5:67:DA:7D:02:43:08:EB:6C:2C:96:FE:41:D9:DE:21:8D",
+ "serialNumber":"0A:01:41:42:00:00:01:53:85:73:6A:0B:85:EC:A7:08"
}
}
}
diff --git a/tails-download-and-verify/conf.json.sample b/tails-download-and-verify/conf.json.sample
new file mode 100644
index 0000000..35e89d5
--- /dev/null
+++ b/tails-download-and-verify/conf.json.sample
@@ -0,0 +1,39 @@
+{
+ "descriptor": "https://tails.boum.org/install/v1/Tails/i386/stable/latest.yml",
+ "fallback_download_url_prefix": "http://dl.amnesia.boum.org/tails",
+ "mirror_pool_url": "https://tails.boum.org/mirrors.json",
+ "pinGlobally": true,
+ "pins": {
+ "domains": {
+ "tails.boum.org": {
+ "certs": null,
+ "issuers": ["Let's Encrypt", "Gandi"]
+ },
+ "labs.riseup.net": {
+ "issuers": ["Let's Encrypt", "Gandi"]
+ }
+ },
+ "certs": {
+ "*.boum.org": {
+ "subjectName":"CN=*.boum.org,OU=Gandi Standard Wildcard SSL,OU=Domain Control Validated",
+ "issuerOrganization":"Gandi",
+ "sha256Fingerprint":"FB:89:1F:85:61:8D:6F:62:EA:A6:6E:92:4D:3A:FC:80:17:03:D6:FB:D5:F4:B0:31:E7:D7:5A:7F:55:06:74:2D",
+ "serialNumber":"00:84:A7:E7:40:C4:D4:54:54:64:E4:35:22:38:F0:29:53"
+ },
+ "Gandi": {
+ "subjectName":"CN=Gandi Standard SSL CA 2,O=Gandi,L=Paris,ST=Paris,C=FR",
+ "issuerOrganization":"The USERTRUST Network",
+ "sha256Fingerprint":"B9:F2:16:43:23:63:8D:CE:0B:92:21:8B:43:C4:1C:1B:2B:26:96:38:93:29:DB:19:F5:CF:7A:D4:9B:5C:B3:72",
+ "serialNumber":"05:E4:DC:3B:94:38:AB:3B:85:97:CB:A6:A1:98:50:E3"
+ },
+ "Let's Encrypt": {
+ "subjectName":"CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US",
+ "commonName":"Let's Encrypt Authority X3",
+ "issuerOrganization":"Digital Signature Trust Co.",
+ "organization":"Let's Encrypt",
+ "sha256Fingerprint":"25:84:7D:66:8E:B4:F0:4F:DD:40:B1:2B:6B:07:40:C5:67:DA:7D:02:43:08:EB:6C:2C:96:FE:41:D9:DE:21:8D",
+ "serialNumber":"0A:01:41:42:00:00:01:53:85:73:6A:0B:85:EC:A7:08"
+ }
+ }
+ }
+}
diff --git a/tails-download-and-verify/data/download-page.js b/tails-download-and-verify/data/download-page.js
index 0c181f6..5a6f434 100644
--- a/tails-download-and-verify/data/download-page.js
+++ b/tails-download-and-verify/data/download-page.js
@@ -193,7 +193,6 @@ initWidgets = (status) => {
}
}
on("a.iso-url", "click", e => {
- e.preventDefault();
if (/-retry$/.test(e.target.id)) return;
Download.cmd('start', { prompt: e.target.title });
});
@@ -201,30 +200,24 @@ initWidgets = (status) => {
for (let action of ['pause', 'resume', 'cancel']) {
let cmd = action;
on(`#download-button-state-${cmd}`, "click", e => {
- e.preventDefault();
- Download.cmd(cmd);
+ if (/\bdownload-again\b/.test(e.target.className)) {
+ Download.reset();
+ } else {
+ Download.cmd(cmd);
+ }
});
}
+ on(".download-again", "click", e => Download.reset());
+ on("#download-button-state-retry", "click", e => Download.retry());
- on("#download-button-state-retry", "click", e => {
- e.preventDefault();
- Download.cmd('retry');
- });
-
- on("#i_have_iso", "click", e => {
- e.preventDefault();
- Download.cmd('verify', { prompt: e.target.title });
- });
+ on("#i_have_iso", "click", e => Download.cmd('verify', { prompt: e.target.title }));
- on("#verify-text-success .btn", "click", e => {
- // if (verifySuccess) Download.cmd('reveal', { path: verifySuccess });
- e.preventDefault();
- Download.cmd('retry');
- });
+ on("#verify-text-success .btn", "click", e => Download.cmd('retry'));
initWidgets = null;
},
Download = {
+ resetting: false,
start() {
toggle("#download-eta, #download-path", false);
@@ -232,6 +225,12 @@ Download = {
document.documentElement.dataset.phase = "started";
this.cmd('init');
},
+
+ reset() {
+ this.resetting = true;
+ this.cmd("reset");
+ },
+
cmd(cmd, data) {
console.log(`Sending command Download.${cmd}`);
self.port.emit("download-control", Object.assign({ cmd, page: document.URL, domain: document.domain }, data));
@@ -267,6 +266,11 @@ Download = {
case 'verified':
updateVerifyView(status);
break;
+ case 'init':
+ if (Download.resetting) {
+ reload();
+ }
+ break;
}
document.documentElement.dataset.phase = status.phase;
diff --git a/tails-download-and-verify/index.js b/tails-download-and-verify/index.js
index 771ee93..4c4861a 100644
--- a/tails-download-and-verify/index.js
+++ b/tails-download-and-verify/index.js
@@ -3,7 +3,7 @@ var self = require('sdk/self');
var downloader = null;
var workers = new Set();
-var Config = require("../conf.json");
+var Config = require("conf.json");
if (Config.pinGlobally) {
require("./lib/cert-pinner").pinGlobally(Config.pins);
@@ -30,6 +30,7 @@ function onUI(msg) {
case "cancel":
case "resume":
case "retry":
+ case "reset":
downloader[msg.cmd].call(downloader, msg.page);
break;
case "verify":
diff --git a/tails-download-and-verify/install.rdf b/tails-download-and-verify/install.rdf
new file mode 100644
index 0000000..3bc8981
--- /dev/null
+++ b/tails-download-and-verify/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>dave@tails.boum.org</em:id>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+ <em:unpack>false</em:unpack>
+ <em:version>0.2.9rc1</em:version>
+ <em:name>Tails Download and Verify</em:name>
+ <em:description>A browser extension to download and verify Tails ISO images</em:description>
+ <em:creator>Giorgio Maone</em:creator>
+ <em:homepageURL>https://tails.boum.org</em:homepageURL>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>38.0a1</em:minVersion>
+ <em:maxVersion>39.0</em:maxVersion>
+</Description>
+</em:targetApplication>
+
+
+ </Description>
+
+</RDF>
diff --git a/tails-download-and-verify/lib/cert-pinner.js b/tails-download-and-verify/lib/cert-pinner.js
index beae8ba..06c7ad2 100644
--- a/tails-download-and-verify/lib/cert-pinner.js
+++ b/tails-download-and-verify/lib/cert-pinner.js
@@ -5,14 +5,25 @@ Cu.import("resource://gre/modules/Services.jsm");
const NS_BINDING_ABORTED = 0x804b0002;
const DOCUMENT_LOAD_FLAGS = Ci.nsIChannel.LOAD_DOCUMENT_URI | Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
-function checkCertificate(cert, known) {
- if (!cert) throw new Error(`No certification bound to channel (pin: "${known.subjectName}")`);
- if (!known) throw new Error(`No certification pinning data for ${cert.subjectName}!`);
- for (let p of Object.keys(known)) {
- if (known[p] !== cert[p]) {
- throw new Error(`Certificate mismatch for ${p}@${cert.subjectName}: "${known[p]}" != "${cert[p]}"!`);
+function checkCertificate(cert, knownCerts) {
+ if (!cert) throw new Error(`No certificate bound to channel (pin: "${known.subjectName}")`);
+ if (!(Array.isArray(knownCerts) && knownCerts.length)) throw new Error(`No certification pinning data for ${cert.subjectName}!`);
+ let mismatches = [];
+ for (let known of knownCerts) {
+ let match = true;
+ for (let p of Object.keys(known)) {
+ if (known[p] !== cert[p]) {
+ mismatches.push(`${p}@${known.subjectName} ("${known[p]}" != "${cert[p]}")`);
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ console.log(`Certificate ${cert.subjectName} matches :)`);
+ return;
}
}
+ throw new Error(`Certificate mismatch: ${cert.subjectName} does not match ${mismatches.join(", ")}!`);
}
function checkChannel(channel, pins, allowUnpinned = false) {
@@ -24,7 +35,7 @@ function checkChannel(channel, pins, allowUnpinned = false) {
throw new Error(`No configured pin for ${host}!`);
}
let pin = pins.domains[host];
- if (!(pin.cert || pin.issuer)) return;
+ if (!(pin.certs || pin.issuers)) return;
let securityInfo = channel.securityInfo;
if (!(securityInfo instanceof Ci.nsITransportSecurityInfo)) {
throw new Error(`No certificate for ${host}!`);
@@ -33,10 +44,23 @@ function checkChannel(channel, pins, allowUnpinned = false) {
.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus.serverCert;
console.log(`Checking pins for ${host}...`);
- if (pin.cert) checkCertificate(cert, pins.certs[pin.cert]);
- if (pin.issuer) {
- if (!cert.issuer) throw new Error(`No issuer for certificate "${cert.subjectName}"@${channel.name}!`);
- checkCertificate(cert.issuer, pins.issuers[pin.issuer]);
+
+ let certs = names => names.map(name => pins.certs[name]);
+
+ if (pin.certs) {
+ checkCertificate(cert, certs(pin.certs));
+ }
+ if (pin.issuers) {
+ let errors = [];
+ for (let parent = cert.issuer; parent; parent = parent.issuer) {
+ try {
+ checkCertificate(parent, certs(pin.issuers));
+ return;
+ } catch (e) {
+ errors.push(e.message);
+ }
+ }
+ throw new Error(errors.length ? errors.join("\n") : `No issuer for certificate "${cert.subjectName}"@${channel.name}!`);
}
}
diff --git a/tails-download-and-verify/lib/downloader.js b/tails-download-and-verify/lib/downloader.js
index f9ee529..35f0bfd 100644
--- a/tails-download-and-verify/lib/downloader.js
+++ b/tails-download-and-verify/lib/downloader.js
@@ -152,7 +152,7 @@ function compareUrlToMirrors(testurl, mirrors, url_suffix=blob.url_suffix) {
for (let m_url = 0; m_url < mirrors.length; m_url++) {
console.log("Mirror URL to compare against " + mirrors.mirrors[m_url].url_prefix + url_suffix);
if (mirrors.mirrors[m_url].url_prefix + url_suffix == testurl && mirrors.mirrors[m_url].weight > 0) {
- console.log("Download URL corresponds to a known mirror URL: " + mirrors.mirrors[m_url].url_prefix);
+ console.log("Download URL corresponds to a known active mirror: " + mirrors.mirrors[m_url].url_prefix);
return m_url; // just return the index.
}
}
@@ -258,8 +258,8 @@ var Verify = {
status.phase = 'verifying';
status.failed = false;
},
- get working() status.phase === 'verifying',
- get done() status.phase === 'verified',
+ get working() { return status.phase === 'verifying' },
+ get done() { return status.phase === 'verified' },
onProgressUnclamped(read, total) {
if (!Verify.working) return;
@@ -524,6 +524,12 @@ var api = {
downloadList.removeView(downloadView);
downloadList = downloadView = null;
}
+ },
+
+ reset() {
+ this.cancel();
+ reset();
+ notify();
}
};
diff --git a/tails-download-and-verify/lib/mirror-dispatcher.js b/tails-download-and-verify/lib/mirror-dispatcher.js
new file mode 100644
index 0000000..ab467b2
--- /dev/null
+++ b/tails-download-and-verify/lib/mirror-dispatcher.js
@@ -0,0 +1,232 @@
+/*
+@licstart
+Copyright (C) 2015-2016 Tails Developers
+
+ The JavaScript code in this page is free software: you can
+ redistribute it and/or modify it under the terms of the GNU
+ General Public License (GNU GPL) as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option)
+ any later version. The code is distributed WITHOUT ANY WARRANTY;
+ without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
+
+ As additional permission under GNU GPL version 3 section 7, you
+ may distribute non-source (e.g., minimized or compacted) forms of
+ that code without the copy of the GNU GPL normally requestuired by
+ section 4, provided you include this license notice and a URL
+ through which recipients can access the Corresponding Source.
+@licend
+*/
+
+'use strict';
+
+/*
+ * To enable console logging in production set DEBUG to true
+ */
+
+var DEBUG = false;
+// Allow debugging through the JS console
+if(DEBUG === false) {
+ var console = { log: function() {} };
+}
+
+/*
+ * Some versions of IE do not support logging to the console.
+ * When we set ALERTFALLBACK to true, we can log to alert.
+ * In any case, if console function does not exist, we do not want to log
+ * nor throw an error, thus disabling console.log.
+ */
+
+var ALERTFALLBACK = false;
+if (typeof console === "undefined" || typeof console.log === "undefined") {
+ console = {};
+ if (ALERTFALLBACK) {
+ console.log = function(msg) {
+ alert(msg);
+ };
+ } else {
+ console.log = function() {};
+ }
+}
+
+function get(path, callback) {
+
+ var max_expected_filelength = 8 * Math.pow(2,10);
+
+ // Do the usual request stuff
+ var request = new XMLHttpRequest();
+ request.open('GET', path);
+
+ // Handle network errors
+ request.onerror = function() {
+ console.log("Network Error");
+ return false;
+ };
+
+ request.onreadystatechange = function() {
+ // Check the status
+ // 4 = Headers and responseText have been loaded.
+ if (request.readyState === 4) {
+ // console.log(request.getAllResponseHeaders());
+ // check filesize is not too big
+ if(request.responseText.length > max_expected_filelength) {
+ console.log("Retrieved content too big. Response length: " + request.responseText.length + " max_expected_filelength: " + max_expected_filelength);
+ request.abort();
+ return false;
+ }
+ // Check TLS
+ if(!/^https:\/\//.test(request.responseURL)) {
+ console.log("File not served over TLS.");
+ request.abort();
+ return false;
+ }
+ // empty string = text responseType
+ if(request.responseType != "") {
+ console.log("Expected responseType is text.");
+ request.abort();
+ return false;
+ }
+
+ // 200 = The request has succeeded.
+ if (request.status === 200) {
+ var data = request.responseText;
+ console.log("data = request.responseText: " + data);
+ if (callback) {
+ callback(data);
+ }
+ } else {
+ console.log(request.statusText);
+ request.abort();
+ return false;
+ }
+ }
+ };
+
+ // Make the request
+ request.send();
+}
+
+/*
+ * Choose a random mirror, based on weight.
+ * weight="0" - this mirror is not active.
+ * There is no max weight.
+ * @params: JSON mirror data, formatted like this:
+ - schema: https://git-tails.immerda.ch/mirror-pool/tree/schema.json
+ - example: https://git-tails.immerda.ch/mirror-pool/tree/example-mirrors.json
+ * @returns false or one randomly chosen host URL prefix
+ */
+function getRandomMirrorUrlPrefix(data) {
+ if (data.mirrors.length > 0) {
+ var end_of_last_range = 0,
+ i,
+ mirrors = [],
+ picked_value,
+ ranges_end = [];
+
+ // Build a "compressed" version of the array that would contain N times each
+ // mirror, with N being its weight. We "compress" it by only storing, for
+ // each mirror, the index of the last element it would occupy in the
+ // uncompressed version of the array.
+ for (i = 0; i < data.mirrors.length; i++) {
+ ranges_end[i]
+ = end_of_last_range
+ = end_of_last_range + data.mirrors[i].weight;
+ }
+
+ // If we were using the "uncompressed" version of the aforementioned array,
+ // we would do something like Math.random() * uncompressed_array.length,
+ // and the result would be the mirror we want. But here we are using
+ // a "compressed" version of this array, so the result we get instead is
+ // the minimum *weight* of the mirror we pick randomly.
+ picked_value = Math.floor(Math.random() * end_of_last_range) + 1;
+
+ // Pick the first mirror that would occupy a range that ends at or after
+ // uncompressed_array[picked_value] in the "uncompressed" version
+ // of the array. I.e. once converted to the "compressed" optimization:
+ // the one whose ranges_end[i] is bigger or equal to picked_value.
+ for (i = 0; i < data.mirrors.length; i++) {
+ if (picked_value <= ranges_end[i]) {
+ if(isValidURL(data.mirrors[i].url_prefix)) {
+ return data.mirrors[i].url_prefix;
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+}
+
+/*
+ * Returns true if url is a valid URL pattern
+ */
+function isValidURL(url) {
+ var url_pattern = new RegExp('^(http|https):\/\/[a-z0-9\-_]+(\.[a-z0-9\-_]+)+([a-z0-9\-_\.,@\?^=%&;:/~\+#]*[a-z0-9\-\_#@\?^=%&;/~\+])?$', 'i');
+ var test = url_pattern.test(url)
+ console.log("Tested URL: " + url + " " + test);
+ return(test);
+}
+
+/*
+ * This is the main function call, to be called by our website
+ */
+function replaceUrlPrefixWithRandomMirror(linkelements) {
+ var data, index, max_expected_linkelements, url_current, fallback_download_url_prefix, url_new, url_prefix;
+
+ max_expected_linkelements = 15;
+ if(linkelements.length > max_expected_linkelements) {
+ console.log(linkelements.length + " exceeds number of expected elements: " + max_expected_linkelements);
+ return false;
+ }
+
+ fallback_download_url_prefix = "http://dl.amnesia.boum.org/tails/";
+
+ return get('/mirrors.json', function(data){
+ if( data !== "undefined" ) {
+ data = JSON.parse(data);
+
+ // pick a random mirror
+ url_prefix = getRandomMirrorUrlPrefix(data);
+ console.log("Random URL prefix: " + url_prefix);
+
+ if(url_prefix) {
+ // replace the default prefix with the newly picked one,
+ // in the href of each linkelement
+ for (index = 0; index < linkelements.length; index++) {
+ // remove possible URL whitespaces and line breaks as created by ikiwiki
+ url_current = linkelements[index].getAttribute('href');
+ url_current = url_current.trim();
+ if (url_current !== 'undefined' && isValidURL(url_current)) {
+ url_new = url_prefix +
+ url_current.slice(fallback_download_url_prefix.length);
+ console.log("index: "+ index + " url_current: " + url_current + " --> url_new:" + url_new);
+ linkelements[index].setAttribute('href', url_new)
+ } else {
+ console.log(url_current + " appears to be malformed.");
+ }
+ }
+ } else {
+ console.log("Could not determine url_prefix.");
+ }
+ }
+ });
+}
+
+/* This function is used by DAVE instead of
+ * replaceUrlPrefixWithRandomMirror().
+ * Use mirror pool configuration and select a random mirror.
+ */
+function transformURL(url, fallback_download_url_prefix, mirrors) {
+ return getRandomMirrorUrlPrefix(mirrors) +
+ url.slice(fallback_download_url_prefix.length);
+}
+
+/*
+ * Assign transformURL to exports, so that it can be used
+ * by tails-transform-mirror-url via nodejs.
+ * Will return target object.
+ */
+
+if(typeof exports !== 'undefined') {
+ // Now we know that we're not running in the browser
+ module.exports.transformURL = transformURL;
+}
diff --git a/tails-download-and-verify/package.json b/tails-download-and-verify/package.json
index 7d853f7..441addd 100644
--- a/tails-download-and-verify/package.json
+++ b/tails-download-and-verify/package.json
@@ -3,7 +3,7 @@
"id": "dave@tails.boum.org",
"name": "tails-download-and-verify",
"homepage": "https://tails.boum.org",
- "version": "0.2.7rc60",
+ "version": "0.2.9rc1",
"description": "A browser extension to download and verify Tails ISO images",
"main": "index.js",
"permissions": {
diff --git a/www/dave.xpi b/www/dave.xpi
index bcd34fe..0d3e7be 100644
--- a/www/dave.xpi
+++ b/www/dave.xpi
Binary files differ
diff --git a/www/download.html b/www/download.html
index 73fc652..ab58443 100644
--- a/www/download.html
+++ b/www/download.html
@@ -250,7 +250,7 @@ For your security, it is very important to also verify your download. We propose
two techniques to do this verification automatically.</p>
<div id="download-and-verify" class="chrome-unsupported">
- <div id="extension-version">0.2.7rc60</div>
+ <div id="extension-version">0.2.9rc1</div>
<div id="undetected-browser">
<p>We failed to detect your browser vendor, maybe because JavaScript is disabled.</p>