diff --git a/mozilla/toolkit/components/protection/Makefile.in b/mozilla/toolkit/components/protection/Makefile.in new file mode 100644 index 00000000000..eb764b5fae3 --- /dev/null +++ b/mozilla/toolkit/components/protection/Makefile.in @@ -0,0 +1,53 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +DIRS = public src + +ifdef ENABLE_TESTS +DIRS += tests +endif + +include $(topsrcdir)/config/rules.mk + diff --git a/mozilla/toolkit/components/protection/content/application.js b/mozilla/toolkit/components/protection/content/application.js new file mode 100644 index 00000000000..95775604f14 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/application.js @@ -0,0 +1,73 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +/** + * A simple helper class that enables us to get or create the + * directory in which our app will store stuff. + */ +function PROT_ApplicationDirectory() { + this.debugZone = "appdir"; + this.appDir_ = G_File.getProfileFile(); + this.appDir_.append(PROT_GlobalStore.getAppDirectoryName()); + G_Debug(this, "Application directory is " + this.appDir_.path); +} + +/** + * @returns Boolean indicating if the directory exists + */ +PROT_ApplicationDirectory.prototype.exists = function() { + return this.appDir_.exists() && this.appDir_.isDirectory(); +} + +/** + * Creates the directory + */ +PROT_ApplicationDirectory.prototype.create = function() { + G_Debug(this, "Creating app directory: " + this.appDir_.path); + try { + this.appDir_.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); + } catch(e) { + G_Error(this, this.appDir_.path + " couldn't be created."); + } +} + +/** + * @returns The nsIFile interface of the directory + */ +PROT_ApplicationDirectory.prototype.getAppDirFileInterface = function() { + return this.appDir_; +} diff --git a/mozilla/toolkit/components/protection/content/enchash-decrypter.js b/mozilla/toolkit/components/protection/content/enchash-decrypter.js new file mode 100644 index 00000000000..6486e0b585a --- /dev/null +++ b/mozilla/toolkit/components/protection/content/enchash-decrypter.js @@ -0,0 +1,544 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// This is the code used to interact with data encoded in the +// goog-black-enchash format. The format is basically a map from +// hashed hostnames to encrypted sequences of regular expressions +// where the encryption key is derived from the hashed +// hostname. Encoding lists like this raises the bar slightly on +// deriving complete table data from the db. This data format is NOT +// our idea; we would've raise the bar higher :) +// +// Anyway, this code is a port of the original C++ implementation by +// Garret. To ease verification, I mirrored that code as closely as +// possible. As a result, you'll see some C++-style variable naming +// and roundabout (C++) ways of doing things. Additionally, I've +// omitted the comments. +// +// This code should not change, except to fix bugs. +// +// TODO: verify that using encodeURI() in getCanonicalHost is OK +// TODO: accommodate other kinds of perl-but-not-javascript qualifiers + + +/** + * This thing knows how to generate lookup keys and decrypt values found in + * a table of type enchash. + */ +function PROT_EnchashDecrypter() { + this.debugZone = "enchashdecrypter"; + this.REs_ = PROT_EnchashDecrypter.REs; + this.hasher_ = new G_CryptoHasher(); + this.base64_ = new G_Base64(); + this.rc4_ = new ARC4(); +} + +PROT_EnchashDecrypter.DATABASE_SALT = "oU3q.72p"; +PROT_EnchashDecrypter.SALT_LENGTH = PROT_EnchashDecrypter.DATABASE_SALT.length; + +PROT_EnchashDecrypter.MAX_DOTS = 5; + +PROT_EnchashDecrypter.REs = {}; +PROT_EnchashDecrypter.REs.FIND_DODGY_CHARS = + new RegExp("[\x01-\x1f\x7f-\xff]+"); +PROT_EnchashDecrypter.REs.FIND_DODGY_CHARS_GLOBAL = + new RegExp("[\x01-\x1f\x7f-\xff]+", "g"); +PROT_EnchashDecrypter.REs.FIND_END_DOTS = new RegExp("^\\.+|\\.+$"); +PROT_EnchashDecrypter.REs.FIND_END_DOTS_GLOBAL = + new RegExp("^\\.+|\\.+$", "g"); +PROT_EnchashDecrypter.REs.FIND_MULTIPLE_DOTS = new RegExp("\\.{2,}"); +PROT_EnchashDecrypter.REs.FIND_MULTIPLE_DOTS_GLOBAL = + new RegExp("\\.{2,}", "g"); +PROT_EnchashDecrypter.REs.FIND_TRAILING_DOTS = new RegExp("\\.+$"); +PROT_EnchashDecrypter.REs.POSSIBLE_IP = + new RegExp("^((?:0x[0-9a-f]+|[0-9\\.])+)$", "i"); +PROT_EnchashDecrypter.REs.FIND_BAD_OCTAL = new RegExp("(^|\\.)0\\d*[89]"); +PROT_EnchashDecrypter.REs.IS_OCTAL = new RegExp("^0[0-7]*$"); +PROT_EnchashDecrypter.REs.IS_DECIMAL = new RegExp("^[0-9]+$"); +PROT_EnchashDecrypter.REs.IS_HEX = new RegExp("^0[xX]([0-9a-fA-F]+)$"); + +// Regexps are given in perl regexp format. Unfortunately, JavaScript's +// library isn't completely compatible. For example, you can't specify +// case-insensitive matching by using (?i) in the expression text :( +// So we manually set this bit with the help of this regular expression. +PROT_EnchashDecrypter.REs.CASE_INSENSITIVE = /\(\?i\)/g; + +/** + * Helper function + * + * @param str String to get chars from + * + * @param n Number of characters to get + * + * @returns String made up of the last n characters of str + */ +PROT_EnchashDecrypter.prototype.lastNChars_ = function(str, n) { + n = -n; + return str.substr(n); +} + +/** + * We have to have our own hex-decoder because decodeURIComponent + * expects UTF-8 (so it will barf on invalid UTF-8 sequences). + * + * @param str String to decode + * + * @returns The decoded string + */ +PROT_EnchashDecrypter.prototype.hexDecode_ = function(str) { + var output = []; + + var i = 0; + while (i < str.length) { + var c = str.charAt(i); + + if (c == "%" && i + 2 < str.length) { + + var asciiVal = Number("0x" + str.charAt(i + 1) + str.charAt(i + 2)); + + if (!isNaN(asciiVal)) { + i += 2; + c = String.fromCharCode(asciiVal); + } + } + + output[output.length] = c; + ++i; + } + + return output.join(""); +} + +/** + * Translate a plaintext enchash value into regular expressions + * + * @param data String containing a decrypted enchash db entry + * + * @returns An array of RegExps + */ +PROT_EnchashDecrypter.prototype.parseRegExps = function(data) { + var res = data.split("\t"); + + G_Debug(this, "Got " + res.length + " regular rexpressions"); + + for (var i = 0; i < res.length; i++) { + // Could have leading (?i); if so, set the flag and strip it + var flags = (this.REs_.CASE_INSENSITIVE.test(res[i])) ? "i" : ""; + res[i] = res[i].replace(this.REs_.CASE_INSENSITIVE, ""); + res[i] = new RegExp(res[i], flags); + } + + return res; +} + +PROT_EnchashDecrypter.prototype.getCanonicalHost = function(str) { + var urlObj = Cc["@mozilla.org/network/standard-url;1"] + .createInstance(Ci.nsIURL); + urlObj.spec = str; + var asciiHost = urlObj.asciiHost; + + var unescaped = this.hexDecode_(asciiHost); + + unescaped = unescaped.replace(this.REs_.FIND_DODGY_CHARS_GLOBAL, "") + .replace(this.REs_.FIND_END_DOTS_GLOBAL, "") + .replace(this.REs_.FIND_MULTIPLE_DOTS_GLOBAL, "."); + + var temp = this.parseIPAddress_(unescaped); + if (temp) + unescaped = temp; + + // TODO: what, exactly is it supposed to escape? This doesn't esecape + // ":", "/", ";", and "?" + var escaped = encodeURI(unescaped); + + var k; + var index = escaped.length; + for (k = 0; k < PROT_EnchashDecrypter.MAX_DOTS + 1; k++) { + temp = escaped.lastIndexOf(".", index - 1); + if (temp == -1) { + break; + } else { + index = temp; + } + } + + if (k == PROT_EnchashDecrypter.MAX_DOTS + 1 && index != -1) { + escaped = escaped.substring(index + 1); + } + + escaped = escaped.toLowerCase(); + return escaped; +} + +PROT_EnchashDecrypter.prototype.parseIPAddress_ = function(host) { + + host = host.replace(this.REs_.FIND_TRAILING_DOTS_GLOBAL, ""); + + if (!this.REs_.POSSIBLE_IP.test(host)) + return ""; + + var parts = host.split("."); + if (parts.length > 4) + return ""; + + var allowOctal = !this.REs_.FIND_BAD_OCTAL.test(host); + + for (var k = 0; k < parts.length; k++) { + var canon; + if (k == parts.length - 1) { + canon = this.canonicalNum_(parts[k], 5 - parts.length, allowOctal); + } else { + canon = this.canonicalNum_(parts[k], 1, allowOctal); + } + if (canon != "") + parts[k] = canon; + } + + return parts.join("."); +} + +PROT_EnchashDecrypter.prototype.canonicalNum_ = function(num, bytes, octal) { + + if (bytes < 0) + return ""; + var temp_num; + + if (octal && this.REs_.IS_OCTAL.test(num)) { + + num = this.lastNChars_(num, 11); + + temp_num = parseInt(num, 8); + if (isNaN(temp_num)) + temp_num = -1; + + } else if (this.REs_.IS_DECIMAL.test(num)) { + + num = this.lastNChars_(num, 32); + + temp_num = parseInt(num, 10); + if (isNaN(temp_num)) + temp_num = -1; + + } else if (this.REs_.IS_HEX.test(num)) { + + num = this.lastNChars_(num, 8); + + temp_num = parseInt(num, 16); + if (isNaN(temp_num)) + temp_num = -1; + + } else { + return ""; + } + + if (temp_num == -1) + return ""; + + var parts = []; + while (bytes--) { + parts.push("" + (temp_num % 256)); + temp_num -= temp_num % 256; + temp_num /= 256; + } + + return parts.join("."); +} + +PROT_EnchashDecrypter.prototype.getLookupKey = function(host) { + var dataKey = PROT_EnchashDecrypter.DATABASE_SALT + host; + dataKey = this.base64_.arrayifyString(dataKey); + + this.hasher_.init(G_CryptoHasher.algorithms.MD5); + var lookupDigest = this.hasher_.updateFromArray(dataKey); + var lookupKey = this.hasher_.digestHex(); + + return lookupKey.toUpperCase(); +} + +PROT_EnchashDecrypter.prototype.decryptData = function(data, host) { + + var ascii = this.base64_.decodeString(data); + var random_salt = ascii.slice(0, PROT_EnchashDecrypter.SALT_LENGTH); + var encrypted_data = ascii.slice(PROT_EnchashDecrypter.SALT_LENGTH); + var temp_decryption_key = + this.base64_.arrayifyString(PROT_EnchashDecrypter.DATABASE_SALT); + temp_decryption_key = temp_decryption_key.concat(random_salt); + temp_decryption_key = + temp_decryption_key.concat(this.base64_.arrayifyString(host)); + + this.hasher_.init(G_CryptoHasher.algorithms.MD5); + this.hasher_.updateFromArray(temp_decryption_key); + var decryption_key = this.base64_.arrayifyString(this.hasher_.digestRaw()); + + this.rc4_.setKey(decryption_key, decryption_key.length); + this.rc4_.crypt(encrypted_data, encrypted_data.length); // Works in-place + + return this.base64_.stringifyArray(encrypted_data); +} + + +/** + * Lame unittesting function + */ +function TEST_PROT_EnchashDecrypter() { + if (G_GDEBUG) { + var z = "enchash UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + // Yes this defies our naming convention, but we copy verbatim from + // the C++ unittest, so lets just keep things clear. + var no_dots = "abcd123;[]"; + var one_dot = "abc.123"; + var two_dots = "two..dots"; + var lots_o_dots = "I have a lovely .... bunch of dots"; + var multi_dots = "dots ... and ... more .... dots"; + var leading_dot = ".leading"; + var trailing_dot = "trailing."; + var trailing_dots = "I love trailing dots...."; + var end_dots = ".dots."; + + var decimal = "1234567890"; + var hex = "0x123452FAf"; + var bad_hex = "0xFF0xGG"; + var octal = "012034056"; + var bad_octal = "012034089"; + var garbage = "lk,.:asdfa-="; + var mixed = "1230x78034"; + var spaces = "123 0xFA 045"; + + var longstr = ""; + for(var k = 0; k < 100; k++) { + longstr += "a"; + } + + var shortstr = "short"; + + var r = PROT_EnchashDecrypter.REs; + var l = new PROT_EnchashDecrypter(); + + // Test regular expressions + function testRE(re, inputValPairs) { + for (var i = 0; i < inputValPairs.length; i += 2) + G_Assert(z, re.test(inputValPairs[i]) == inputValPairs[i + 1], + "RegExp broken: " + re + " (input: " + inputValPairs[i] + ")"); + }; + + var tests = + ["", false, + "normal chars;!@#$%^&*&(", false, + "MORE NORMAL ,./<>?;':{}", false, + "Slightly less\2 normal", true, + "\245 stuff \45", true, + "\31", true]; + testRE(r.FIND_DODGY_CHARS, tests); + + tests = + [no_dots, false, + one_dot, false, + leading_dot, true, + trailing_dots, true, + end_dots, true]; + testRE(r.FIND_END_DOTS, tests); + + tests = + [no_dots, false, + one_dot, false, + two_dots, true, + lots_o_dots, true, + multi_dots, true]; + testRE(r.FIND_MULTIPLE_DOTS, tests); + + tests = + [no_dots, false, + one_dot, false, + trailing_dot, true, + trailing_dots, true]; + testRE(r.FIND_TRAILING_DOTS, tests); + + tests = + ["random junk", false, + "123.45.6-7.89", false, + "012.12.123", true, + "0x12.0xff.123", true, + "225.0.0.1", true]; + testRE(r.POSSIBLE_IP, tests); + + tests = + [decimal, false, + hex, false, + octal, false, + bad_octal, true]; + testRE(r.FIND_BAD_OCTAL, tests); + + tests = + [decimal, false, + hex, false, + bad_octal, false, + garbage, false, + mixed, false, + spaces, false, + octal, true]; + testRE(r.IS_OCTAL, tests); + + tests = + [hex, false, + garbage, false, + mixed, false, + spaces, false, + octal, true, + bad_octal, true, + decimal, true]; + testRE(r.IS_DECIMAL, tests); + + tests = + [decimal, false, + octal, false, + bad_octal, false, + garbage, false, + mixed, false, + spaces, false, + bad_hex, false, + hex, true]; + testRE(r.IS_HEX, tests); + + // Test find last N + var val = l.lastNChars_(longstr, 8); + G_Assert(z, val.length == 8, "find last eight broken on long str"); + val = l.lastNChars_(shortstr, 8); + G_Assert(z, val.length == 5, "find last 11 broken on short str"); + + // Test canonical num + tests = + ["", "", 1, true, + "", "10", 0, true, + "", "0x45", -1, true, + "45", "45", 1, true, + "16", "0x10", 1, true, + "111.1", "367", 2, true, + "229.20.0", "012345", 3, true, + "123", "0173", 1, true, + "9", "09", 1, false, + "", "0x120x34", 2, true, + "252.18", "0x12fc", 2, true]; + for (var i = 0; i < tests.length; i+= 4) + G_Assert(z, tests[i] === l.canonicalNum_(tests[i + 1], + tests[i + 2], + tests[i + 3]), + "canonicalNum broken on: " + tests[i + 1]); + + // Test parseIPAddress + var testing = {}; + testing["123.123.0.0.1"] = ""; + testing["255.0.0.1"] = "255.0.0.1"; + testing["12.0x12.01234"] = "12.18.156.2"; + testing["276.2.3"] = "20.2.3.0"; + testing["012.034.01.055"] = "10.28.1.45"; + testing["0x12.0x43.0x44.0x01"] = "18.67.68.1"; + + for (var key in testing) + G_Assert(z, l.parseIPAddress_(key) === testing[key], + "parseIPAddress broken on " + key + "(got: " + + l.parseIPAddress_(key)); + + // Test getCanonicalHost + var testing = {}; + testing["completely.bogus.url.with.a.whole.lot.of.dots"] = + "with.a.whole.lot.of.dots"; + testing["http://poseidon.marinet.gr/~elani"] = "poseidon.marinet.gr"; + testing["http://www.google.com.."] = "www.google.com"; + testing["https://www.yaho%6F.com"] = "www.yahoo.com"; + testing["http://012.034.01.0xa"] = "10.28.1.10"; + testing["ftp://wierd..chars...%0f,%fa"] = "wierd.chars.,"; + for (var key in testing) + G_Assert(z, l.getCanonicalHost(key) == testing[key], + "getCanonicalHost broken on: " + key + + "(got: " + l.getCanonicalHost(key) + ")"); + + // Test getlookupkey + var testing = {}; + testing["www.google.com"] = "AF5638A09FDDDAFF5B7A6013B1BE69A9"; + testing["poseidon.marinet.gr"] = "01844755C8143C4579BB28DD59C23747"; + testing["80.53.164.26"] = "B775DDC22DEBF8BEBFEAC24CE40A1FBF"; + + for (var key in testing) + G_Assert(z, l.getLookupKey(key) === testing[key], + "getlookupkey broken on " + key + " (got: " + + l.getLookupKey(key) + ", expected: " + + testing[key] + ")"); + + // Test decryptdata + var tests = + [ "bGtEQWJuMl/z2ZxSBB2hsuWI8geMAwfSh3YBfYPejQ1O+wyRAJeJ1UW3V56zm" + + "EpUvnaEiECN1pndxW5rEMNzE+gppPeel7PvH+OuabL3NXlspcP0xnpK8rzNgB1" + + "JT1KcajQ9K3CCl24T9r8VGb0M3w==", + "80.53.164.26", + "^(?i)http\\:\\/\\/80\\.53\\.164\\.26(?:\\:80)?\\/\\.PayPal" + + "\\.com\\/webscr\\-id\\/secure\\-SSL\\/cmd\\-run\\=\\/login\\.htm$", + + "ZTMzZjVnb3WW1Yc2ABorgQGAwYfcaCb/BG3sMFLTMDvOQxH8LkdGGWqp2tI5SK" + + "uNrXIHNf2cyzcVocTqUIUkt1Ud1GKieINcp4tWcU53I0VZ0ZZHCjGObDCbv9Wb" + + "CPSx1eS8vMREDv8Jj+UVL1yaZQ==", + "80.53.164.26", + "^(?i)http\\:\\/\\/80\\.53\\.164\\.26(?:\\:80)?\\/\\.PayPal\\.com" + + "\\/webscr\\-id\\/secure\\-SSL\\/cmd\\-run\\=\\/login\\.htm$", + + "ZTMzZjVnb3WVb6VqoJ44hVo4V77XjDRcXTxOc2Zpn4yIHcpS0AQ0nn1TVlX4MY" + + "IeNL/6ggzCmcJSWOOkj06Mpo56LNLrbxNxTBuoy9GF+xcm", + "poseidon.marinet.gr", + "^(?i)http\\:\\/\\/poseidon\\.marinet\\.gr(?:\\:80)?\\/\\~eleni" + + "\\/eBay\\/index\\.php$", + + "bGtEQWJuMl9FA3Kl5RiXMpgFU8nDJl9J0hXjUck9+mMUQwAN6llf0gJeY5DIPP" + + "c2f+a8MSBFJN17ANGJZl5oZVsQfSW4i12rlScsx4tweZAE", + "poseidon.marinet.gr", + "^(?i)http\\:\\/\\/poseidon\\.marinet\\.gr(?:\\:80)?\\/\\~eleni" + + "\\/eBay\\/index\\.php$"]; + + for (var i = 0; i < tests.length; i += 3) { + var dec = l.decryptData(tests[i], tests[i + 1]); + G_Assert(z, dec === tests[i + 2], + "decryptdata broken on " + tests[i] + " (got: " + dec + + ", expected: " + tests[i + 2] + ")"); + } + + + G_Debug(z, "PASSED"); + } +} + diff --git a/mozilla/toolkit/components/protection/content/globalstore.js b/mozilla/toolkit/components/protection/content/globalstore.js new file mode 100644 index 00000000000..0b6d087ef89 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/globalstore.js @@ -0,0 +1,100 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// A class that encapsulates globals such as the names of things. We +// centralize everything here mainly so as to ease their modification, +// but also in case we want to version. +// +// This class does _not_ embody semantics, defaults, or the like. If we +// need something that does, we'll add our own preference registry. +// +// TODO: The code needs to fail more gracefully if these values aren't set +// E.g., createInstance should fail for listmanager without these. + +/** + * A clearinghouse for globals. All interfaces are read-only. + */ +function PROT_GlobalStore() { +} + +/** + * Read a pref value + */ +PROT_GlobalStore.getPref_ = function(prefname) { + var pref = new G_Preferences(); + return pref.getPref(prefname); +} + +/** + * @returns The name of the directory in which we should store data, e.g., + * protection_data + */ +PROT_GlobalStore.getAppDirectoryName = function() { + return PROT_GlobalStore.getPref_("protection.datadirectory"); +} + +/** + * @returns String giving url to use for updates, e.g., + * http://www.google.com/safebrowsing/update? + */ +PROT_GlobalStore.getUpdateserverURL = function() { + // TODO: handle multiple providers + return PROT_GlobalStore.getPref_("protection.provider.0.updateurl"); +} + +/** + * @returns String giving url to use for re-keying, e.g., + * https://www.google.com/safebrowsing/getkey? + */ +PROT_GlobalStore.getGetKeyURL = function() { + return PROT_GlobalStore.getPref_("protection.provider.0.keyurl"); +} + +/** + * @returns String giving filename to use to store keys + */ +PROT_GlobalStore.getKeyFilename = function() { + return "kf.txt"; +} + +/** + * For running unittests. + * TODO: make this automatic based on build rules + */ +PROT_GlobalStore.isTesting = function() { + return true; +} diff --git a/mozilla/toolkit/components/protection/content/js/arc4.js b/mozilla/toolkit/components/protection/content/js/arc4.js new file mode 100644 index 00000000000..c99acc6b6d4 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/js/arc4.js @@ -0,0 +1,125 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Marius Schilder (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +/** + * ARC4 streamcipher implementation + * @constructor + */ +function ARC4() { + this._S = new Array(256); + this._i = 0; + this._j = 0; +} + +/** + * Initialize the cipher for use with new key. + * @param {byte[]} key is byte array containing key + * @param {int} opt_length indicates # of bytes to take from key + */ +ARC4.prototype.setKey = function(key, opt_length) { + if (!isArray(key)) { + throw new Error("Key parameter must be a byte array"); + } + + if (!opt_length) { + opt_length = key.length; + } + + var S = this._S; + + for (var i = 0; i < 256; ++i) { + S[i] = i; + } + + var j = 0; + for (var i = 0; i < 256; ++i) { + j = (j + S[i] + key[i % opt_length]) & 255; + + var tmp = S[i]; + S[i] = S[j]; + S[j] = tmp; + } + + this._i = 0; + this._j = 0; +} + +/** + * Discard n bytes of the keystream. + * These days 1536 is considered a decent amount to drop to get + * the key state warmed-up enough for secure usage. + * This is not done in the constructor to preserve efficiency for + * use cases that do not need this. + * @param {int} n is # of bytes to disregard from stream + */ +ARC4.prototype.discard = function(n) { + var devnul = new Array(n); + this.crypt(devnul); +} + +/** + * En- or decrypt (same operation for streamciphers like ARC4) + * @param {byte[]} data gets xor-ed in place + * @param {int} opt_length indicated # of bytes to crypt + */ +ARC4.prototype.crypt = function(data, opt_length) { + if (!opt_length) { + opt_length = data.length; + } + + if (!isArray(data)) { + throw new Error("Data parameter must be a byte array"); + } + + var i = this._i; + var j = this._j; + var S = this._S; + + for (var n = 0; n < opt_length; ++n) { + i = (i + 1) & 255; + j = (j + S[i]) & 255; + + var tmp = S[i]; + S[i] = S[j]; + S[j] = tmp; + + data[n] ^= S[(S[i] + S[j]) & 255]; + } + + this._i = i; + this._j = j; +} diff --git a/mozilla/toolkit/components/protection/content/js/lang.js b/mozilla/toolkit/components/protection/content/js/lang.js new file mode 100644 index 00000000000..10824088d6a --- /dev/null +++ b/mozilla/toolkit/components/protection/content/js/lang.js @@ -0,0 +1,362 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Aaron Boodman (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// This file has pure js helper functions. Hence you'll find metion +// of browser-specific features in here. + + +/** + * lang.js - The missing JavaScript language features + * + * WARNING: This class adds members to the prototypes of String, Array, and + * Function for convenience. + * + * The tradeoff is that the for/in statement will not work properly for those + * objects when this library is used. + * + * To work around this for Arrays, you may want to use the forEach() method, + * which is more fun and easier to read. + */ + +/** + * Returns true if the specified value is not |undefined|. + */ +function isDef(val) { + return typeof val != "undefined"; +} + +/** + * Returns true if the specified value is |null| + */ +function isNull(val) { + return val === null; +} + +/** + * Returns true if the specified value is an array + */ +function isArray(val) { + return isObject(val) && val.constructor == Array; +} + +/** + * Returns true if the specified value is a string + */ +function isString(val) { + return typeof val == "string"; +} + +/** + * Returns true if the specified value is a boolean + */ +function isBoolean(val) { + return typeof val == "boolean"; +} + +/** + * Returns true if the specified value is a number + */ +function isNumber(val) { + return typeof val == "number"; +} + +/** + * Returns true if the specified value is a function + */ +function isFunction(val) { + return typeof val == "function"; +} + +/** + * Returns true if the specified value is an object + */ +function isObject(val) { + return val && typeof val == "object"; +} + +/** + * Returns an array of all the properties defined on an object + */ +function getObjectProps(obj) { + var ret = []; + + for (var p in obj) { + ret.push(p); + } + + return ret; +} + +/** + * Returns true if the specified value is an object which has no properties + * defined. + */ +function isEmptyObject(val) { + if (!isObject(val)) { + return false; + } + + for (var p in val) { + return false; + } + + return true; +} + +var getHashCode; +var removeHashCode; + +(function () { + var hashCodeProperty = "lang_hashCode_"; + + /** + * Adds a lang_hashCode_ field to an object. The hash code is unique for the + * given object. + * @param obj {Object} The object to get the hash code for + * @returns {Number} The hash code for the object + */ + getHashCode = function(obj) { + // In IE, DOM nodes do not extend Object so they do not have this method. + // we need to check hasOwnProperty because the proto might have this set. + if (obj.hasOwnProperty && obj.hasOwnProperty(hashCodeProperty)) { + return obj[hashCodeProperty]; + } + if (!obj[hashCodeProperty]) { + obj[hashCodeProperty] = ++getHashCode.hashCodeCounter_; + } + return obj[hashCodeProperty]; + }; + + /** + * Removes the lang_hashCode_ field from an object. + * @param obj {Object} The object to remove the field from. + */ + removeHashCode = function(obj) { + obj.removeAttribute(hashCodeProperty); + }; + + getHashCode.hashCodeCounter_ = 0; +})(); + +/** + * Fast prefix-checker. + */ +String.prototype.startsWith = function(prefix) { + if (this.length < prefix.length) { + return false; + } + + if (this.substring(0, prefix.length) == prefix) { + return true; + } + + return false; +} + +/** + * Removes whitespace from the beginning and end of the string + */ +String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g, ""); +} + +/** + * Does simple python-style string substitution. + * "foo%s hot%s".subs("bar", "dog") becomes "foobar hotdot". + * For more fully-featured templating, see template.js. + */ +String.prototype.subs = function() { + var ret = this; + + // this appears to be slow, but testing shows it compares more or less equiv. + // to the regex.exec method. + for (var i = 0; i < arguments.length; i++) { + ret = ret.replace(/\%s/, String(arguments[i])); + } + + return ret; +} + +/** + * Returns the last element on an array without removing it. + */ +Array.prototype.peek = function() { + return this[this.length - 1]; +} + +// TODO(anyone): add splice the first time someone needs it and then implement +// push, pop, shift, unshift in terms of it where possible. + +// TODO(anyone): add the other neat-o functional methods like map(), etc. + +/** + * Partially applies this function to a particular "this object" and zero or + * more arguments. The result is a new function with some arguments of the first + * function pre-filled and the value of |this| "pre-specified". + * + * Remaining arguments specified at call-time are appended to the pre- + * specified ones. + * + * Also see: partial(). + * + * Note that bind and partial are optimized such that repeated calls to it do + * not create more than one function object, so there is no additional cost for + * something like: + * + * var g = bind(f, obj); + * var h = partial(g, 1, 2, 3); + * var k = partial(h, a, b, c); + * + * Usage: + * var barMethBound = bind(myFunction, myObj, "arg1", "arg2"); + * barMethBound("arg3", "arg4"); + * + * @param thisObj {object} Specifies the object which |this| should point to + * when the function is run. If the value is null or undefined, it will default + * to the global object. + * + * @returns {function} A partially-applied form of the function bind() was + * invoked as a method of. + */ +function bind(fn, self, opt_args) { + var boundargs = isDef(fn.boundArgs_) ? fn.boundArgs_ : []; + boundargs = boundargs.concat(Array.prototype.slice.call(arguments, 2)); + + if (isDef(fn.boundSelf_)) { + self = fn.boundSelf_; + } + + if (isDef(fn.boundFn_)) { + fn = fn.boundFn_; + } + + var newfn = function() { + // Combine the static args and the new args into one big array + var args = boundargs.concat(Array.prototype.slice.call(arguments)); + return fn.apply(self, args); + } + + newfn.boundArgs_ = boundargs; + newfn.boundSelf_ = self; + newfn.boundFn_ = fn; + + return newfn; +} + +/** + * An alias to the bind() global function. + * + * Usage: + * var g = f.bind(obj, arg1, arg2); + * g(arg3, arg4); + */ +Function.prototype.bind = function(self, opt_args) { + return bind.apply( + null, [this, self].concat(Array.prototype.slice.call(arguments, 1))); +} + +/** + * Like bind(), except that a "this object" is not required. Useful when the + * target function is already bound. + * + * Usage: + * var g = partial(f, arg1, arg2); + * g(arg3, arg4); + */ +function partial(fn, opt_args) { + return bind.apply( + null, [fn, null].concat(Array.prototype.slice.call(arguments, 1))); +} + +/** + * An alias to the partial() global function. + * + * Usage: + * var g = f.partial(arg1, arg2); + * g(arg3, arg4); + */ +Function.prototype.partial = function(opt_args) { + return bind.apply( + null, [this, null].concat(Array.prototype.slice.call(arguments))); +} + +/** + * Convenience. Binds all the methods of obj to itself. Calling this in the + * constructor before referencing any methods makes things a little more like + * Java or Python where methods are intrinsically bound to their instance. + */ +function bindMethods(obj) { + for (var p in obj) { + if (isFunction(obj[p])) { + obj[p] = obj[p].bind(obj); + } + } +} + +/** + * Inherit the prototype methods from one constructor into another. + * + * Usage: + *
+ * function ParentClass(a, b) { }
+ * ParentClass.prototype.foo = function(a) { }
+ *
+ * function ChildClass(a, b, c) {
+ *   ParentClass.call(this, a, b);
+ * }
+ *
+ * ChildClass.inherits(ParentClass);
+ *
+ * var child = new ChildClass("a", "b", "see");
+ * child.foo(); // works
+ * 
+ * + * In addition, a superclass' implementation of a method can be invoked + * as follows: + * + *
+ * ChildClass.prototype.foo = function(a) {
+ *   ChildClass.superClass_.foo.call(this, a);
+ *   // other code
+ * };
+ * 
+ */ +Function.prototype.inherits = function(parentCtor) { + var tempCtor = function(){}; + tempCtor.prototype = parentCtor.prototype; + this.superClass_ = parentCtor.prototype; + this.prototype = new tempCtor(); +} diff --git a/mozilla/toolkit/components/protection/content/js/thread-queue.js b/mozilla/toolkit/components/protection/content/js/thread-queue.js new file mode 100644 index 00000000000..5f2ee7ac5fa --- /dev/null +++ b/mozilla/toolkit/components/protection/content/js/thread-queue.js @@ -0,0 +1,221 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Ojan Vafai (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +/** + * Simulates a thread in JavaScript. However, since there is no preemption in + * it's not a proper thread. Specifically, any worker function that takes + * longer than the allotted runtime will simply take up that extra time. It is + * the responsibility of the user of this class to ensure that each iteration + * of a worker function runs in less time than the necessary runtime. + * + * The delay and runtime (ms) settings control how much the thread affects the + * user experience. Larger runtime will mean the computation happens faster, + * but the user will notice more jumpiness. A larger delay can mitigate + * jumpiness significantly and causes the computation to take longer. + * + * The interleave setting means that the thread interleaves the running of + * the workers that have been added to it. If this value is false, each worker + * runs to completion before the next worker starts running (a worker + * that does not complete will cause all subsequent workers to never run). + * + * Dependencies: + * - alarm.js (only when used inside a Firefox service) + * + * @param opt_config Configuration object with optional settings for runtime, + * delay and noInterleave. + */ +function TH_ThreadQueue(opt_config) { + // default values + this.runtime_ = 100; + this.delay_ = 25; + this.interleave_ = true; + + if (opt_config) { + this.interleave_ = (opt_config.interleave === true); + if (opt_config.runtime) this.runtime_ = opt_config.runtime; + if (opt_config.delay) this.delay_ = opt_config.delay; + } + + this.workers_ = []; + this.guid_ = 0; + + // Bind this here so that it doesn't need to be bound on every iteration. + // We are using this clugy way of calling the method becuase of a Firefox + // XUL bug . This used + // to use BindToObject but that has been depreciated. + this.iterate_ = Function.prototype.bind.call(this.runIteration_, this); +} + +/** + * Add a worker function to the thread. Adding more workers means that each + * worker runs slower and the user will notice more jumpiness. + * + * @param worker Function that iteratively computes the task. Must not + * already be a worker in the thread. + * @param opt_isComplete Function that identifies if this worker is complete. + * If it is complete, then this worker is removed from + * the thread. If omitted, this worker will never + * complete. + */ +TH_ThreadQueue.prototype.addWorker = function(worker, opt_isComplete) { + if (worker.__workerId) { + throw new Error("Cannot add a worker to a thread while it is currently " + + "running in the thread."); + } + + worker.__isComplete = opt_isComplete || function() { return false; }; + worker.__workerId = this.guid_++; + this.workers_.push(worker); + if (this.workers_.length == 1) { + this.setCurrentWorkerByIndex_(0); + // If thread is paused iterate_ is a noop. + this.iterate_(); + } +} + +/** + * Remove a worker from this thread. + * + * @param worker The worker function to remove from the thread. This must be + * the same function object that was passed into add worker, + * not just the same function. + */ +TH_ThreadQueue.prototype.removeWorker = function(worker) { + for (var i = 0; i < this.workers_.length; i++) { + if (worker.__workerId == this.workers_[i].__workerId) { + this.removeWorkerByIndex_(i); + } + } +} + +/** + * Remove a worker based on its index in the workers array. + */ +TH_ThreadQueue.prototype.removeWorkerByIndex_ = function(workerIndex) { + var worker = this.workers_[workerIndex]; + delete(worker.__isComplete); + delete(worker.__workerId); + + this.workers_.splice(workerIndex, 1); + + var newWorkerIndex = this.currentWorkerIndex_ % this.workers_.length; + this.setCurrentWorkerByIndex_(newWorkerIndex); +} + +/** +* Set the delay between iterations of the thread. +* +* @param delay Delay in milliseconds +*/ +TH_ThreadQueue.prototype.setDelay = function(delay) { + this.delay_ = delay; +} + +/** + * Set the runtime of each iteration of the thread. Note that this runtime + * cannot be 100% assured and will definitely not be kept if any worker takes + * longer than the runtime to run an iteration. + * + * @param runtime Runtime in milliseconds + */ +TH_ThreadQueue.prototype.setRuntime = function(runtime) { + this.runtime_ = runtime; +} + +/** + * Set the thread running. + */ +TH_ThreadQueue.prototype.run = function() { + this.running_ = true; + this.iterate_(); +} + +/** + * Pause the thread. + */ +TH_ThreadQueue.prototype.pause = function() { + this.running_ = false; +} + +/** + * Run an iteration of the thread. + * Does nothing if there are no workers, or the thread is paused. + */ +TH_ThreadQueue.prototype.runIteration_ = function() { + if (!this.running_ || this.workers_.length == 0) { + return; + } + + var startTime = (new Date()).getTime(); + + while ((new Date()).getTime() - startTime < this.runtime_) { + if (this.currentWorker_.__isComplete()) { + this.removeWorkerByIndex_(this.currentWorkerIndex_); + break; + } + + this.currentWorker_(); + if (this.interleave_) this.nextWorker_(); + } + + if (typeof top != "undefined" && top.setTimeout) { + top.setTimeout(this.iterate_, this.delay_); + } else if (G_Alarm) { + new G_Alarm(this.iterate_, this.delay_); + } else { + throw new Error("Could not find a mechanism to start a timeout. Need " + + "window.setTimeout or G_Alarm."); + } +} + +/** + * Set the current worker to be the worker the currentWorkerIndex_ points to. + * + * @param workerIndex Index of the worker into the workers_ array. + */ +TH_ThreadQueue.prototype.setCurrentWorkerByIndex_ = function(workerIndex) { + this.currentWorkerIndex_ = workerIndex; + this.currentWorker_ = this.workers_[workerIndex]; +} + +/** +* Iterate to the next worker function. +*/ +TH_ThreadQueue.prototype.nextWorker_ = function() { + var nextWorkerIndex = (this.currentWorkerIndex_ + 1) % this.workers_.length; + this.setCurrentWorkerByIndex_(nextWorkerIndex); +} diff --git a/mozilla/toolkit/components/protection/content/list-warden.js b/mozilla/toolkit/components/protection/content/list-warden.js new file mode 100644 index 00000000000..2be5e4d50d6 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/list-warden.js @@ -0,0 +1,225 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Niels Provos (original author)d + + * Fritz Schneider + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// A warden that knows how to register lists with a listmanager and keep them +// updated if necessary. The ListWarden also provides a simple interface to +// check if a URL is evil or not. Specialized wardens like the PhishingWarden +// inherit from it. +// +// Classes that inherit from ListWarden are responsible for calling +// enableTableUpdates or disableTableUpdates. This usually entails +// registering prefObservers and calling enable or disable in the base +// class as appropriate. +// + +/** + * Abtracts the checking of user/browser actions for signs of + * phishing. + * + * @param ListManager ListManager that allows us to check against white + * and black lists. + * + * @constructor + */ +function PROT_ListWarden(listManager) { + this.debugZone = "listwarden"; + this.listManager_ = listManager; + + // Use this to query preferences + this.prefs_ = new G_Preferences(); + + // Once we register tables, their respective names will be listed here. + this.blackTables_ = []; + this.whiteTables_ = []; + this.sandboxUpdates_ = []; +} + +/** + * Tell the ListManger to keep all of our tables updated + */ + +PROT_ListWarden.prototype.enableTableUpdates = function() { + this.listManager_.enableUpdateTables(this.blackTables_); + this.listManager_.enableUpdateTables(this.whiteTables_); +} + +/** + * Tell the ListManager to stop updating our tables + */ + +PROT_ListWarden.prototype.disableTableUpdates = function() { + this.listManager_.disableUpdateTables(this.blackTables_); + this.listManager_.disableUpdateTables(this.whiteTables_); +} + +/** + * Tell the ListManager to keep sandbox updated. Don't tie this to the wl/bl + * updates. + */ +PROT_ListWarden.prototype.enableSandboxUpdates = function() { + this.listManager_.enableUpdateTables(this.sandboxUpdates_); +} + +/** + * Tell the ListManager not to keep sandbox updated. Don't tie this to the + * wl/bl updates. + */ +PROT_ListWarden.prototype.disableSandboxUpdates = function() { + this.listManager_.disableUpdateTables(this.sandboxUpdates_); +} + +/** + * Register a new black list table with the list manager + * @param tableName - name of the table to register + * @returns true if the table could be registered, false otherwise + */ + +PROT_ListWarden.prototype.registerBlackTable = function(tableName) { + var result = this.listManager_.registerTable(tableName); + if (result) + this.blackTables_.push(tableName); + return result; +} + +/** + * Register a new white list table with the list manager + * @param tableName - name of the table to register + * @returns true if the table could be registered, false otherwise + */ + +PROT_ListWarden.prototype.registerWhiteTable = function(tableName) { + var result = this.listManager_.registerTable(tableName); + if (result) + this.whiteTables_.push(tableName); + return result; +} + +/** + * Internal method that looks up a url in a set of tables + * + * @param tables array of table names + * @param url URL to look up + * @returns Boolean indicating if the url is in one of the tables + */ +PROT_ListWarden.prototype.isURLInTables_ = function(tables, url) { + for (var i = 0; i < tables.length; ++i) { + if (this.listManager_.safeLookup(tables[i], url)) + return true; + } + return false; +} + +/** + * Internal method that looks up a url in the white list. + * + * @param url URL to look up + * @returns Boolean indicating if the url is on our whitelist + */ +PROT_ListWarden.prototype.isWhiteURL_ = function(url) { + if (!this.listManager_) + return false; + + var whitelisted = this.isURLInTables_(this.whiteTables_, url); + + if (whitelisted) + G_Debug(this, "Whitelist hit: " + url); + + return whitelisted; +} + +/** + * Internal method that looks up a url in the black lists. + * + * @param url URL to look up + * @returns Boolean indicating if the url is on our blacklist(s) + */ +PROT_ListWarden.prototype.isBlackURL_ = function(url) { + if (!this.listManager_) + return false; + + var blacklisted = this.isURLInTables_(this.blackTables_, url); + + if (blacklisted) + G_Debug(this, "Blacklist hit: " + url); + + return blacklisted; +} + +/** + * Internal method that looks up a url in both the white and black lists. + * + * If there is conflict, the white list has precedence over the black list. + * + * @param url URL to look up + * @returns Boolean indicating if the url is phishy. + */ +PROT_ListWarden.prototype.isEvilURL_ = function(url) { + return !this.isWhiteURL_(url) && this.isBlackURL_(url); +} + +// Some unittests +// TODO something more appropriate + +function TEST_PROT_ListWarden() { + if (G_GDEBUG) { + var z = "listwarden UNITTEST"; + G_debugService.enableZone(z); + G_Debug(z, "Starting"); + + var threadQueue = new TH_ThreadQueue(); + var listManager = new PROT_ListManager(threadQueue, true /* testing */); + + var warden = new PROT_ListWarden(listManager); + + // Just some really simple test + G_Assert(z, warden.registerWhiteTable("test-white-domain"), + "Failed to register test-white-domain table"); + G_Assert(z, warden.registerWhiteTable("test-black-url"), + "Failed to register test-black-url table"); + listManager.safeInsert("test-white-domain", "http://foo.com/good", "1"); + listManager.safeInsert("test-black-url", "http://foo.com/good/1", "1"); + listManager.safeInsert("test-black-url", "http://foo.com/bad/1", "1"); + + G_Assert(z, !warden.isEvilURL_("http://foo.com/good/1"), + "White listing is not working."); + G_Assert(z, warden.isEvilURL_("http://foo.com/bad/1"), + "Black listing is not working."); + + G_Debug(z, "PASSED"); + } +} diff --git a/mozilla/toolkit/components/protection/content/listmanager.js b/mozilla/toolkit/components/protection/content/listmanager.js new file mode 100644 index 00000000000..f010ebc1504 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/listmanager.js @@ -0,0 +1,747 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Niels Provos (original author) + * Fritz Schneider + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// A class that manages lists, namely white and black lists for +// phishing or malware protection. The ListManager knows how to fetch, +// update, and store lists, and knows the "kind" of list each is (is +// it a whitelist? a blacklist? etc). However it doesn't know how the +// lists are serialized or deserialized (the wireformat classes know +// this) nor the specific format of each list. For example, the list +// could be a map of domains to "1" if the domain is phishy. Or it +// could be a map of hosts to regular expressions to match, who knows? +// Answer: the trtable knows. List are serialized/deserialized by the +// wireformat reader from/to trtables, and queried by the listmanager. +// +// There is a single listmanager for the whole application. +// +// The listmanager is used only in privacy mode; in advanced protection +// mode a remote server is queried. +// +// How to add a new table: +// 1) get it up on the server +// 2) add it to tablesKnown +// 3) if it is not a known table type (trtable.js), add an implementation +// for it in trtable.js +// 4) add a check for it in the phishwarden's isXY() method, for example +// isBlackURL() +// +// TODO: obviously the way this works could use a lot of improvement. In +// particular adding a list should just be a matter of adding +// its name to the listmanager and an implementation to trtable +// (or not if a talbe of that type exists). The format and semantics +// of the list comprise its name, so the listmanager should easily +// be able to figure out what to do with what list (i.e., no +// need for step 4). +// TODO reading, writing, and whatnot code should be cleaned up +// TODO read/write asynchronously +// TODO more comprehensive update tests, for example add unittest check +// that the listmanagers tables are properly written on updates + + +/** + * A ListManager keeps track of black and white lists and knows + * how to update them. + * + * @constructor + */ +function PROT_ListManager() { + this.debugZone = "listmanager"; + G_debugService.enableZone(this.debugZone); + + // We run long-lived operations on "threads" (user-level + // time-slices) in order to prevent locking up the UI + var threadConfig = { + "interleave": true, + "runtime": 80, + "delay": 400, + }; + this.threadQueue_ = new TH_ThreadQueue(threadConfig); + this.appDir_ = null; // appDir is set via a method call + this.currentUpdateChecker_ = null; // set when we toggle updates + this.rpcPending_ = false; + this.readingData_ = false; + + // We don't want to start checking against our lists until we've + // read them. But then again, we don't want to queue URLs to check + // forever. So if we haven't successfully read our lists after a + // certain amount of time, just pretend. + this.dataImportedAtLeastOnce_ = false; + var self = this; + new G_Alarm(function() { self.dataImportedAtLeastOnce_ = true; }, 60 * 1000); + + // TOOD(tc): PREF + this.updateserverURL_ = PROT_GlobalStore.getUpdateserverURL(); + + // The lists we know about and the parses we can use to read + // them. Default all to the earlies possible version (1.-1); this + // version will get updated when successfully read from disk or + // fetch updates. + this.tablesKnown_ = {}; + + if (PROT_GlobalStore.isTesting()) { + // populate with some tables for unittesting + this.tablesKnown_ = { + // "goog-white-domain": new PROT_VersionParser("goog-white-domain", 1, -1), + // "goog-black-url" : new PROT_VersionParser("goog-black-url", 1, -1), + // "goog-black-enchash": new PROT_VersionParser("goog-black-enchash", 1, -1), + // "goog-white-url": new PROT_VersionParser("goog-white-url", 1, -1), + // + // A major version of zero means local, so don't ask for updates + "test1-foo-domain" : new PROT_VersionParser("test1-foo-domain", 0, -1), + "test2-foo-domain" : new PROT_VersionParser("test2-foo-domain", 0, -1), + "test-white-domain" : + new PROT_VersionParser("test-white-domain", 0, -1, true /* require mac*/), + "test-mac-domain" : + new PROT_VersionParser("test-mac-domain", 0, -1, true /* require mac */) + }; + + // expose the object for unittesting + this.wrappedJSObject = this; + } + + this.tablesData = {}; + + this.urlCrypto_ = null; +} + +/** + * Register a new table table + * @param tableName - the name of the table + * @param opt_requireMac true if a mac is required on update, false otherwise + * @returns true if the table could be created; false otherwise + */ +PROT_ListManager.prototype.registerTable = function(tableName, + opt_requireMac) { + var table = new PROT_VersionParser(tableName, 1, -1, opt_requireMac); + if (!table) + return false; + this.tablesKnown_[tableName] = table; + + return true; +} + +/** + * Enable updates for some tables + * @param tables - an array of table names that need updating + */ +PROT_ListManager.prototype.enableUpdateTables = function(tables) { + var changed = false; + for (var i = 0; i < tables.length; ++i) { + var table = this.tablesKnown_[tables[i]]; + if (table) { + G_Debug(this, "Enabling table updates for " + tables[i]); + table.needsUpdate = true; + changed = true; + } + } + + if (changed === true) + this.maybeToggleUpdateChecking(); +} + +/** + * Disables updates for some tables + * @param tables - an array of table names that no longer need updating + */ +PROT_ListManager.prototype.disableUpdateTables = function(tables) { + var changed = false; + for (var i = 0; i < tables.length; ++i) { + var table = this.tablesKnown_[tables[i]]; + if (table) { + G_Debug(this, "Disabling table updates for " + tables[i]); + table.needsUpdate = false; + changed = true; + } + } + + if (changed === true) + this.maybeToggleUpdateChecking(); +} + +/** + * Determine if we have some tables that need updating. + */ +PROT_ListManager.prototype.requireTableUpdates = function() { + for (var type in this.tablesKnown_) { + // All tables with a major of 0 are internal tables that we never + // update remotely. + if (this.tablesKnown_[type].major == 0) + continue; + + // Tables that need updating even if other tables dont require it + if (this.tablesKnown_[type].needsUpdate) + return true; + } + + return false; +} + +/** + * Start managing the lists we know about. We don't do this automatically + * when the listmanager is instantiated because their profile directory + * (where we store the lists) might not be available. + */ +PROT_ListManager.prototype.maybeStartManagingUpdates = function() { + if (PROT_GlobalStore.isTesting()) + return; + + // We might have been told about tables already, so see if we should be + // actually updating. + this.maybeToggleUpdateChecking(); +} + +/** + * Determine if we have any tables that require updating. Different + * Wardens may call us with new tables that need to be updated. + */ +PROT_ListManager.prototype.maybeToggleUpdateChecking = function() { + // If we are testing or dont have an application directory yet, we should + // not start reading tables from disk or schedule remote updates + if (PROT_GlobalStore.isTesting() || !this.appDir_) + return; + + // We update tables if we have some tables that want updates. If there + // are no tables that want to be updated - we dont need to check anything. + if (this.requireTableUpdates() === true) { + G_Debug(this, "Starting managing lists"); + // Read new table data if necessary - if we already have data we just + // skip reading from disk + new G_Alarm(BindToObject(this.readDataFiles, this), 0); + // Multiple warden can ask us to reenable updates at the same time, but we + // really just need to schedule a single update. + if (!this.currentUpdateChecker_) + this.currentUpdateChecker_ = + new G_Alarm(BindToObject(this.checkForUpdates, this), 3000); + this.startUpdateChecker(); + } else { + G_Debug(this, "Stopping managing lists (if currently active)"); + this.stopUpdateChecker(); // Cancel pending updates + } +} + +/** + * Start periodic checks for updates. Idempotent. + */ +PROT_ListManager.prototype.startUpdateChecker = function() { + this.stopUpdateChecker(); + + // Schedule a check for updates every so often + // TODO(tc): PREF NEW + var sixtyMinutes = 60 * 60 * 1000; + this.updateChecker_ = new G_Alarm(BindToObject(this.checkForUpdates, this), + sixtyMinutes, true /* repeat */); +} + +/** + * Stop checking for updates. Idempotent. + */ +PROT_ListManager.prototype.stopUpdateChecker = function() { + if (this.updateChecker_) { + this.updateChecker_.cancel(); + this.updateChecker_ = null; + } +} + +/** + * Set the directory in which we should serialize data + * + * @param appDir An nsIFile pointing to our directory (should exist) + */ +PROT_ListManager.prototype.setAppDir = function(appDir) { + G_Debug(this, "setAppDir: " + appDir.path); + this.appDir_ = appDir; +} + +/** + * Clears the specified table + * @param table Name of the table that we want to consult + * @returns true if the table exists, false otherwise + */ +PROT_ListManager.prototype.clearList = function(table) { + if (!this.tablesKnown_[table]) + return false; + + this.tablesData[table] = newProtectionTable(this.tablesKnown_[table].type); + return true; +} + +/** + * Provides an exception free way to look up the data in a table. We + * use this because at certain points our tables might not be loaded, + * and querying them could throw. + * + * @param table Name of the table that we want to consult + * @param key Key for table lookup + * @returns false or the value in the table corresponding to key. + * If the table name does not exist, we return false, too. + */ +PROT_ListManager.prototype.safeLookup = function(table, key) { + var result = false; + try { + var map = this.tablesData[table]; + result = map.find(key); + } catch(e) { + result = false; + G_Debug(this, "Safelookup masked failure for " + table + ", key " + key + ": " + e); + } + + return result; +} + +/** + * Provides an exception free way to insert data into a table. + * @param table Name of the table that we want to consult + * @param key Key for table insert + * @param value Value for table insert + * @returns true if the value could be inserted, false otherwise + */ +PROT_ListManager.prototype.safeInsert = function(table, key, value) { + if (!this.tablesKnown_[table]) { + G_Debug(this, "Unknown table: " + table); + return false; + } + if (!this.tablesData[table]) + this.tablesData[table] = newProtectionTable(table); + try { + this.tablesData[table].insert(key, value); + } catch (e) { + G_Debug(this, "Cannot insert key " + key + " value " + value); + G_Debug(this, e); + return false; + } + + return true; +} + +/** + * Provides an exception free way to remove data from a table. + * @param table Name of the table that we want to consult + * @param key Key for table erase + * @returns true if the value could be removed, false otherwise + */ +PROT_ListManager.prototype.safeErase = function(table, key) { + if (!this.tablesKnown_[table]) { + G_Debug(this, "Unknown table: " + table); + return false; + } + + if (!this.tablesData[table]) + return false; + + return this.tablesData[table].erase(key); +} + +PROT_ListManager.prototype.getTable = function(tableName) { + return this.tablesData[tableName]; +} + +/** + * Returns a list of files that we should try to read. We do not return + * tables that already have data or that we tried to read in the past. + * @returns array of file names + */ + +PROT_ListManager.prototype.listUnreadDataFiles = function() { + // Now we need to read all of our nice data files. + var files = []; + for (var type in this.tablesKnown_) { + var table = this.tablesKnown_[type]; + // Do not read data for tables that we already know about + if (this.tablesData[type] || table.didRead === true) + continue; + + // Tables that dont require updates are not read from disk either + if (table.needsUpdate === false) + continue; + + var filename = type + ".sst"; + var file = this.appDir_.clone(); + file.append(filename); + if (file.exists() && file.isFile() && file.isReadable()) { + G_Debug(this, "Found saved data for: " + type); + files.push(file); + } else { + G_Debug(this, "Failed to find saved data for: " + type); + } + } + + return files; +} + +/** + * Reads all data files from storage + * @returns true if we started reading data from disk, false otherwise. + */ +PROT_ListManager.prototype.readDataFiles = function() { + if (this.readingData_ === true) { + G_Debug(this, "Already reading data from disk"); + return true; + } + + if (this.rpcPending_ === true) { + G_Debug(this, "Cannot read data files while an update RPC is pending"); + new G_Alarm(BindToObject(this.readDataFiles, this), 10 * 1000); + return false; + } + + // If we have no files there is nothing more for us todo + var files = this.listUnreadDataFiles(); + if (!files.length) + return true; + + this.readingData_ = true; + + // Remember that we attempted to read from all tables + for (var type in this.tablesKnown_) + this.tablesKnown_[type].didRead = true; + + // TODO: Should probably break this up on a thread + var data = ""; + for (var i = 0; i < files.length; ++i) { + G_Debug(this, "Trying to read: " + files[i].path); + var gFile = new G_FileReader(files[i]); + data += gFile.read() + "\n"; + gFile.close(); + } + + this.deserialize_(data, BindToObject(this.dataFromDisk, this)); + return true; +} + +/** + * Creates a WireFormatReader and calls deserialize on it. + */ +PROT_ListManager.prototype.deserialize_ = function(data, callback) { + var wfr = new PROT_WireFormatReader(this.threadQueue_, this.tablesData); + wfr.deserialize(data, callback); +} + +/** + * A callback that is executed when we have read our table data from + * disk. + * + * @param tablesKnown An array that maps table name to current version + * @param tablesData An array that maps a table name to a Map which + * contains key value pairs. + */ +PROT_ListManager.prototype.dataFromDisk = function(tablesKnown, tablesData) { + G_Debug(this, "Called dataFromDisk"); + + this.importData_(tablesKnown, tablesData); + + this.readingData_ = false; + + // If we have more files to read schedule another round of reading + var files = this.listUnreadDataFiles(); + if (files.length) + new G_Alarm(BindToObject(this.readDataFiles, this), 0); + + return true; +} + +/** + * Prepares a URL to fetch upates from. Format is a squence of + * type:major:minor, fields + * + * @param url The base URL to which query parameters are appended; assumes + * already has a trailing ? + * @returns the URL that we should request the table update from. + */ +PROT_ListManager.prototype.getRequestURL_ = function(url) { + url += "version="; + var firstElement = true; + var requestMac = false; + + for(var type in this.tablesKnown_) { + // All tables with a major of 0 are internal tables that we never + // update remotely. + if (this.tablesKnown_[type].major == 0) + continue; + + // Check if the table needs updating + if (this.tablesKnown_[type].needsUpdate == false) + continue; + + if (!firstElement) { + url += "," + } else { + firstElement = false; + } + url += type + ":" + this.tablesKnown_[type].toUrl(); + + if (this.tablesKnown_[type].requireMac) + requestMac = true; + } + + // Request a mac only if at least one of the tables to be updated requires + // it + if (requestMac) { + // Add the wrapped key for requesting macs + if (!this.urlCrypto_) + this.urlCrypto_ = new PROT_UrlCrypto(); + + url += "&wrkey=" + + encodeURIComponent(this.urlCrypto_.getManager().getWrappedKey()); + } + + G_Debug(this, "getRequestURL returning: " + url); + return url; +} + +/** + * Updates our internal tables from the update server + * + * @returns true when a new request was scheduled, false if an old request + * was still pending. + */ +PROT_ListManager.prototype.checkForUpdates = function() { + // Allow new updates to be scheduled from maybeToggleUpdateChecking() + this.currentUpdateChecker_ = null; + + if (this.rpcPending_) { + G_Debug(this, 'checkForUpdates: old callback is still pending...'); + return false; + } + + if (this.readingData_) { + G_Debug(this, 'checkForUpdate: still reading data from disk...'); + + // Reschedule the update to happen in a little while + new G_Alarm(BindToObject(this.checkForUpdates, this), 500); + return false; + } + + G_Debug(this, 'checkForUpdates: scheduling request..'); + this.rpcPending_ = true; + this.xmlFetcher_ = new PROT_XMLFetcher(); + this.xmlFetcher_.get(this.getRequestURL_(this.updateserverURL_), + BindToObject(this.rpcDone, this)); + return true; +} + +/** + * A callback that is executed when the XMLHTTP request is finished + * + * @param data String containing the returned data + */ +PROT_ListManager.prototype.rpcDone = function(data) { + G_Debug(this, "Called rpcDone"); + /* Runs in a thread and executes the callback when ready */ + this.deserialize_(data, BindToObject(this.dataReady, this)); + + this.xmlFetcher_ = null; +} + +/** + * @returns Boolean indicating if it has read data from somewhere (e.g., + * disk) + */ +PROT_ListManager.prototype.hasData = function() { + return !!this.dataImportedAtLeastOnce_; +} + +/** + * Check if the mac passed, if required. + * + * @param oldParser The current version parser for the table + * @param newParser A version parser returned by the WireFormatReader + * + * @returns true if no mac is required, or if it is and the newParser says it + * didn't fail, false otherwise + */ +PROT_ListManager.prototype.maybeCheckMac = function(oldParser, newParser) { + if (!oldParser.requireMac) + return true; + + if (newParser.mac && !newParser.macFailed) + return true; + + G_Debug(this, "mac required and it failed"); + return false; +} + +/** + * We've deserialized tables from disk or the update server, now let's + * swap them into the tables we're currently using. + * + * @param tablesKnown An array that maps table name to current version + * @param tablesData An array that maps a table name to a Map which + * contains key value pairs. + * @returns Array of strings holding the names of tables we updated + */ +PROT_ListManager.prototype.importData_ = function(tablesKnown, tablesData) { + this.dataImportedAtLeastOnce_ = true; + + var changes = []; + + // If our data has changed, update it + if (tablesKnown && tablesData) { + // Update our tables with the new data + for (var name in tablesKnown) { + // WireFormatReader constructs VersionParsers from scratch and doesn't + // know about the requireMac flag, so check it now + if (tablesKnown[name] && this.tablesKnown_[name] && + this.tablesKnown_[name] != tablesKnown[name] && + this.maybeCheckMac(this.tablesKnown_[name], tablesKnown[name])) { + changes.push(name); + + this.tablesKnown_[name].ImportVersion(tablesKnown[name]); + this.tablesData[name] = tablesData[name]; + } + } + } + + return changes; +} + +/** + * A callback that is executed when we have updated the tables from + * the server. We are provided with the new table versions and the + * corresponding data. + * + * @param tablesKnown An array that maps table name to current version + * @param tablesData An array that maps a table name to a Map which + * contains key value pairs. + */ +PROT_ListManager.prototype.dataReady = function(tablesKnown, tablesData) { + G_Debug(this, "Called dataReady"); + + // First, replace the current tables we're using + var changes = this.importData_(tablesKnown, tablesData); + + // Then serialize the new tables to disk + if (changes.length) { + G_Debug(this, "Committing " + changes.length + " changed tables to disk."); + for (var i = 0; i < changes.length; i++) { + this.storeTable(changes[i], + this.tablesData[changes[i]], + this.tablesKnown_[changes[i]]); + } + } + + this.rpcPending_ = false; // todo maybe can do away cuz asynch + G_Debug(this, "Done writing data to disk."); +} + +/** + * Serialize a table to disk. + * + * @param tableName String holding the name of the table to serialize + * + * @param opt_table Reference to the Map holding the table (if omitted, + * we look the table up) + * + * @param opt_parser Reference to the versionparser for this table (if + * omitted we look the table up) + */ +PROT_ListManager.prototype.storeTable = function(tableName, + opt_table, + opt_parser) { + + var table = opt_table ? opt_table : this.tablesData[tableName]; + var parser = opt_parser ? opt_parser : this.tablesKnown_[tableName]; + + if (!table || ! parser) + G_Error(this, "Tried to serialize a non-existent table: " + tableName); + + var wfw = new PROT_WireFormatWriter(this.threadQueue_); + wfw.serialize(table, parser, BindToObject(this.writeDataFile, + this, + tableName)); +} + +/** + * Takes a serialized table and writes it into our application directory. + * + * @param tableName String containing the name of the table, used to + * create the filename + * + * @param tableData Serialized version of the table + */ +PROT_ListManager.prototype.writeDataFile = function(tableName, tableData) { + var filename = tableName + ".sst"; + + G_Debug(this, "Serializing to " + filename); + + try { + var tmpFile = G_File.createUniqueTempFile(filename); + var tmpFileWriter = new G_FileWriter(tmpFile); + + tmpFileWriter.write(tableData); + tmpFileWriter.close(); + + } catch(e) { + G_Debug(this, e); + G_Error(this, "Couldn't write to temp file: " + filename); + } + + // Now overwrite! + try { + tmpFile.moveTo(this.appDir_, filename); + } catch(e) { + G_Debug(this, e); + G_Error(this, "Couldn't overwrite existing table: " + + tmpFile.path + ', ' + + this.appDir_.path + ', ' + filename); + tmpFile.remove(false /* not recursive */); + } + G_Debug(this, "Serializing to " + filename + " finished."); +} + +PROT_ListManager.prototype.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsISample) || + iid.equals(Components.interfaces.nsIProtectionListManager)) + return this; + + Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE; + return null; +} + +// A simple factory function that creates nsIProtectionTable instances based +// on a name. The name is a string of the format +// provider_name-semantic_type-table_type. For example, goog-white-enchash +// or goog-black-url. +function newProtectionTable(name) { + G_Debug("protfactory", "Trying to create a new nsIProtectionTable: " + name); + var tokens = name.split('-'); + var type = tokens[2]; + var table = Cc['@mozilla.org/protection/protectiontable;1?type=' + type] + .createInstance(Ci.nsIProtectionTable); + table.name = name; + return table; +} diff --git a/mozilla/toolkit/components/protection/content/map.js b/mozilla/toolkit/components/protection/content/map.js new file mode 100644 index 00000000000..ecb7d194582 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/map.js @@ -0,0 +1,232 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// This is a map over which you can iterate (you can't get an iterator +// for an Object used as a map). It is important to have an iterable +// map if you have a large numbe ofkeys and you wish to process them +// in chunks, for example on a thread. +// +// This Map's operations are all O(1) plus the time of the native +// operations on its arrays and object. The tradeoff for this speed is +// that the size of the map is always non-decreasing. That is, when an +// item is erase()'d, it is merely marked as invalid and not actually +// removed. +// +// This map is not safe if your keys are objects (they'll get +// stringified to the same value in non-debug builds). +// +// Interface: +// insert(key, value) +// erase(key) +// find(key) +// getList() // This is our poor man's iterator +// forEach(someFunc) +// replace(otherMap) // Clones otherMap, replacing the current map +// size // readonly attribute +// +// TODO: we could easily have this map periodically compact itself so as +// to eliminate the size tradeoff. + +/** + * Create a new Map + * + * @constructor + * @param opt_name A string used to name the map + */ +function G_Map(opt_name) { + this.debugZone = "map"; + this.name = "noname"; + + // We store key/value pairs sequentially in membersArray + this.membersArray_ = []; + + // In membersMap we map from keys to indices in the membersArray + this.membersMap_ = {}; + this.size_ = 0; + + // set to true to allow list manager to update + this.needsUpdate = false; +} + +/** + * Add an item + * + * @param key An key to add (overwrites previous key) + * @param value The value to store at that key + */ +G_Map.prototype.insert = function(key, value) { + if (key == null) + throw new Error("Can't use null as a key"); + else if (typeof key == "object") + throw new Error("This class is not objectsafe; use ObjectSafeMap"); + if (value === undefined) + throw new Error("Can't store undefined values in this map"); + + // Get rid of the old value, if there was one, and increment size if not + var oldIndexInArray = this.membersMap_[key]; + if (typeof oldIndexInArray == "number") + this.membersArray_[oldIndexInArray] = undefined; + else + this.size_++; + + var indexInArray = this.membersArray_.length; + this.membersArray_.push({ "key": key, "value": value}); + this.membersMap_[key] = indexInArray; +} + +/** + * Remove a key from the map + * + * @param key The key to remove + * @returns Boolean indicating if the key was removed + */ +G_Map.prototype.erase = function(key) { + var indexInArray = this.membersMap_[key]; + + if (indexInArray === undefined) + return false; + + this.size_--; + delete this.membersMap_[key]; + // We could slice here, but that could be expensive if the map large + this.membersArray_[indexInArray] = undefined; + + return true; +} + +/** + * Private lookup function + * + * @param key The key to look up + * @returns The value at that key or undefined if it doesn't exist + */ +G_Map.prototype.find_ = function(key) { + var indexInArray = this.membersMap_[key]; + return ((indexInArray === undefined) ? + undefined : + this.membersArray_[indexInArray].value); +} + +/** + * Public lookup function + * + * @param key The key to look up + * @returns The value at that key or undefined if it doesn't exist + */ +G_Map.prototype.find = function(key) { + return this.find_(key); +} + +/** + * Return the list of the keys we have. Use findValue to get the associated + * value. + * + * TODO: its probably better to expose a real iterator, but this works + * TODO: getting keys and values is a pain, do something more clever + * + * @returns Array of items in the map, some entries of which might be + * undefined. Each item is an object with members key and + * value + */ +G_Map.prototype.getKeys = function(count) { + var ret = []; + for (var i = 0; i < this.membersArray_.length; ++i) { + ret.push(this.membersArray_[i].key); + } + count.value = ret.length; + return ret; +} + +G_Map.prototype.findValue = function(key) { + return this.find_(key); +} + +/** + * Replace one map with the content of another + * + * IMPORTANT NOTE: This method copies references to objects, it does + * NOT copy the objects themselves. + * + * @param other Reference to a G_Map that we should clone + */ +G_Map.prototype.replace = function(other) { + this.membersMap_ = {}; + this.membersArray_ = []; + + // TODO Make this faster, and possibly run it on a thread + var lst = {}; + var otherList = other.getList(lst); + for (var i = 0; i < otherList.length; i++) + if (otherList[i]) { + var index = this.membersArray_.length; + this.membersArray_.push(otherList[i]); + this.membersMap_[otherList[i].key] = index; + } + this.size_ = other.size; +} + +/** + * Apply a function to each of the key value pairs. + * + * @param func Function to apply to the map's key value pairs + */ +G_Map.prototype.forEach = function(func) { + if (typeof func != "function") + throw new Error("argument to forEach is not a function, it's a(n) " + + typeof func); + + for (var i = 0; i < this.membersArray_.length; i++) + if (this.membersArray_[i]) + func(this.membersArray_[i].key, this.membersArray_[i].value); +} + +G_Map.prototype.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsISample) || + iid.equals(Components.interfaces.nsIProtectionTable)) + return this; + + Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE; + return null; +} + +/** + * Readonly size attribute. + * @returns The number of keys in the map + */ +G_Map.prototype.__defineGetter__('size', function() { + return this.size_; +}); diff --git a/mozilla/toolkit/components/protection/content/moz/alarm.js b/mozilla/toolkit/components/protection/content/moz/alarm.js new file mode 100644 index 00000000000..cd2f1e81bd9 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/alarm.js @@ -0,0 +1,176 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// An Alarm fires a callback after a certain amount of time, or at +// regular intervals. It's a convenient replacement for +// setTimeout/Interval when you don't want to bind to a specific +// window. +// +// The ConditionalAlarm is an Alarm that cancels itself if its callback +// returns a value that type-converts to true. +// +// Example: +// +// function foo() { alert('hi'); }; +// new G_Alarm(foo, 10*1000); // Fire foo in 10 seconds +// new G_Alarm(foo, 10*1000, true /*repeat*/); // Fire foo every 10 seconds +// new G_Alarm(foo, 10*1000, true, 7); // Fire foo every 10 seconds +// // seven times +// new G_ConditionalAlarm(foo, 1000, true); // Fire every sec until foo()==true +// +// // Fire foo every 10 seconds until foo returns true or until it fires seven +// // times, whichever happens first. +// new G_ConditionalAlarm(foo, 10*1000, true /*repeating*/, 7); +// +// TODO: maybe pass an isFinal flag to the callback if they opted to +// set maxTimes and this is the last iteration? + + +/** + * Set an alarm to fire after a given amount of time, or at specific + * intervals. + * + * @param callback Function to call when the alarm fires + * @param delayMS Number indicating the length of the alarm period in ms + * @param opt_repeating Boolean indicating whether this should fire + * periodically + * @param opt_maxTimes Number indicating a maximum number of times to + * repeat (obviously only useful when opt_repeating==true) + */ +function G_Alarm(callback, delayMS, opt_repeating, opt_maxTimes) { + this.debugZone = "alarm"; + this.callback_ = callback; + this.repeating_ = !!opt_repeating; + this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + var type = opt_repeating ? + this.timer_.TYPE_REPEATING_SLACK : + this.timer_.TYPE_ONE_SHOT; + this.maxTimes_ = opt_maxTimes ? opt_maxTimes : null; + this.nTimes_ = 0; + + this.observerServiceObserver_ = new G_ObserverServiceObserver( + 'xpcom-shutdown', + BindToObject(this.cancel, this)); + + // Ask the timer to use nsITimerCallback (.notify()) when ready + this.timer_.initWithCallback(this, delayMS, type); +} + +/** + * Cancel this timer + */ +G_Alarm.prototype.cancel = function() { + if (!this.timer_) { + return; + } + + this.timer_.cancel(); + // Break circular reference created between this.timer_ and the G_Alarm + // instance (this) + this.timer_ = null; + this.callback_ = null; + + // We don't need the shutdown observer anymore + this.observerServiceObserver_.unregister(); +} + +/** + * Invoked by the timer when it fires + * + * @param timer Reference to the nsITimer which fired (not currently + * passed along) + */ +G_Alarm.prototype.notify = function(timer) { + // fire callback and save results + var ret = this.callback_(); + + // If they've given us a max number of times to fire, enforce it + this.nTimes_++; + if (this.repeating_ && + typeof this.maxTimes_ == "number" + && this.nTimes_ >= this.maxTimes_) { + this.cancel(); + } else if (!this.repeating_) { + // Clear out the callback closure for TYPE_ONE_SHOT timers + this.cancel(); + } + // We don't cancel/cleanup timers that repeat forever until either + // xpcom-shutdown occurs or cancel() is called explicitly. + + return ret; +} + +/** + * XPCOM cruft + */ +G_Alarm.prototype.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsITimerCallback)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; +} + + +/** + * An alarm with the additional property that it cancels itself if its + * callback returns true. + * + * For parameter documentation, see G_Alarm + */ +function G_ConditionalAlarm(callback, delayMS, opt_repeating, opt_maxTimes) { + G_Alarm.call(this, callback, delayMS, opt_repeating, opt_maxTimes); + this.debugZone = "conditionalalarm"; +} + +G_ConditionalAlarm.inherits(G_Alarm); + +/** + * Invoked by the timer when it fires + * + * @param timer Reference to the nsITimer which fired (not currently + * passed along) + */ +G_ConditionalAlarm.prototype.notify = function(timer) { + // Call G_Alarm::notify + var rv = G_Alarm.prototype.notify.call(this, timer); + + if (this.repeating_ && rv) { + G_Debug(this, "Callback of a repeating alarm returned true; cancelling."); + this.cancel(); + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/base64.js b/mozilla/toolkit/components/protection/content/moz/base64.js new file mode 100644 index 00000000000..d49aebc90a3 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/base64.js @@ -0,0 +1,293 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// Base64 en/decoding. Not much to say here except that we work with +// decoded values in arrays of bytes. By "byte" I mean a number in [0, +// 255]. + + +/** + * Base64 en/decoder. Useful in contexts that don't have atob/btoa, or + * when you need a custom encoding function (e.g., websafe base64). + * + * @constructor + */ +function G_Base64() { + this.byteToCharMap_ = {}; + this.charToByteMap_ = {}; + this.byteToCharMapWebSafe_ = {}; + this.charToByteMapWebSafe_ = {}; + this.init_(); +} + +/** + * Our default alphabet. Value 64 (=) is special; it means "nothing." + */ +G_Base64.ENCODED_VALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/="; + +/** + * Our websafe alphabet. Value 64 (=) is special; it means "nothing." + */ +G_Base64.ENCODED_VALS_WEBSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789-_="; + +/** + * We want quick mappings back and forth, so we precompute two maps. + */ +G_Base64.prototype.init_ = function() { + for (var i = 0; i < G_Base64.ENCODED_VALS.length; i++) { + this.byteToCharMap_[i] = G_Base64.ENCODED_VALS.charAt(i); + this.charToByteMap_[this.byteToCharMap_[i]] = i; + this.byteToCharMapWebSafe_[i] = G_Base64.ENCODED_VALS_WEBSAFE.charAt(i); + this.charToByteMapWebSafe_[this.byteToCharMapWebSafe_[i]] = i; + } +} + +/** + * Base64-encode an array of bytes. + * + * @param input An array of bytes (numbers with value in [0, 255]) to encode + * + * @param opt_webSafe Boolean indicating we should use the alternative alphabet + * + * @returns String containing the base64 encoding + */ +G_Base64.prototype.encodeByteArray = function(input, opt_webSafe) { + + if (!(input instanceof Array)) + throw new Error("encodeByteArray takes an array as a parameter"); + + var byteToCharMap = opt_webSafe ? + this.byteToCharMapWebSafe_ : + this.byteToCharMap_; + + var output = []; + + var i = 0; + while (i < input.length) { + + var byte1 = input[i]; + var haveByte2 = i + 1 < input.length; + var byte2 = haveByte2 ? input[i + 1] : 0; + var haveByte3 = i + 2 < input.length; + var byte3 = haveByte3 ? input[i + 2] : 0; + + var outByte1 = byte1 >> 2; + var outByte2 = ((byte1 & 0x03) << 4) | (byte2 >> 4); + var outByte3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6); + var outByte4 = byte3 & 0x3F; + + if (!haveByte3) { + outByte4 = 64; + + if (!haveByte2) + outByte3 = 64; + } + + output.push(byteToCharMap[outByte1]); + output.push(byteToCharMap[outByte2]); + output.push(byteToCharMap[outByte3]); + output.push(byteToCharMap[outByte4]); + + i += 3; + } + + return output.join(""); +} + +/** + * Base64-decode a string. + * + * @param input String to decode + * + * @param opt_webSafe Boolean indicating we should use the alternative alphabet + * + * @returns Array of bytes representing the decoded value. + */ +G_Base64.prototype.decodeString = function(input, opt_webSafe) { + + if (input.length % 4) + throw new Error("Length of b64-encoded data must be zero mod four"); + + var charToByteMap = opt_webSafe ? + this.charToByteMapWebSafe_ : + this.charToByteMap_; + + var output = []; + + var i = 0; + while (i < input.length) { + + var byte1 = charToByteMap[input.charAt(i)]; + var byte2 = charToByteMap[input.charAt(i + 1)]; + var byte3 = charToByteMap[input.charAt(i + 2)]; + var byte4 = charToByteMap[input.charAt(i + 3)]; + + if (byte1 === undefined || byte2 === undefined || + byte3 === undefined || byte4 === undefined) + throw new Error("String contains characters not in our alphabet: " + + input); + + var outByte1 = (byte1 << 2) | (byte2 >> 4); + output.push(outByte1); + + if (byte3 != 64) { + var outByte2 = ((byte2 << 4) & 0xF0) | (byte3 >> 2); + output.push(outByte2); + + if (byte4 != 64) { + var outByte3 = ((byte3 << 6) & 0xC0) | byte4; + output.push(outByte3); + } + } + + i += 4; + } + + return output; +} + +/** + * Helper function that turns a string into an array of numbers. + * + * @param str String to arrify + * + * @returns Array holding numbers corresponding to the UCS character codes + * of each character in str + */ +G_Base64.prototype.arrayifyString = function(str) { + var output = []; + for (var i = 0; i < str.length; i++) + output.push(str.charCodeAt(i)); + return output; +} + +/** + * Helper function that turns an array of numbers into the string + * given by the concatenation of the characters to which the numbesr + * correspond (got that?). + * + * @param array Array of numbers representing characters + * + * @returns Stringification of the array + */ +G_Base64.prototype.stringifyArray = function(array) { + var output = []; + for (var i = 0; i < array.length; i++) + output[i] = String.fromCharCode(array[i]); + return output.join(""); +} + + +/** + * Lame unittesting function + */ +function TEST_G_Base64() { + if (G_GDEBUG) { + var z = "base64 UNITTEST"; + G_debugService.enableZone(z); + G_Debug(z, "Starting"); + + var b = new G_Base64(); + + // Let's see if it's sane by feeding it some well-known values. Index i + // has the input and index i+1 has the expected value. + + var tests = + [ "", "", + "f", "Zg==", + "fo", "Zm8=", + "foo", "Zm9v", + "foob", "Zm9vYg==", + "fooba", "Zm9vYmE=", + "foobar", "Zm9vYmFy"]; + + for (var i = 0; i < tests.length; i += 2) { + var enc = b.encodeByteArray(b.arrayifyString(tests[i])); + G_Assert(z, enc === tests[i + 1], + "Error encoding: " + tests[i] + " (got " + enc + + " but wanted " + tests[i + 1] + ")"); + var dec = b.stringifyArray(b.decodeString(enc)); + G_Assert(z, dec === tests[i], + "Error deocding " + enc + " (got " + dec + + " but wanted " + tests[i] + ")"); + } + + // Now run it through its paces + + var numIterations = 100; + for (var i = 0; i < numIterations; i++) { + + var input = []; + for (var j = 0; j < i; j++) + input[j] = j % 256; + + var encoded = b.encodeByteArray(input); + var decoded = b.decodeString(encoded); + G_Assert(z, !(encoded.length % 4), "Encoded length not a multiple of 4?"); + G_Assert(z, input.length == decoded.length, + "Decoded length not equal to input length?"); + + for (var j = 0; j < i; j++) + G_Assert(z, input[j] === decoded[j], "Values differ at position " + j); + } + + // Test non-websafe / websafe difference + var test = ">>>???>>>???"; + var enc = b.encodeByteArray(b.arrayifyString(test)); + G_Assert(z, enc == "Pj4+Pz8/Pj4+Pz8/", "Non-websafe broken?"); + enc = b.encodeByteArray(b.arrayifyString(test), true /* websafe */); + G_Assert(z, enc == "Pj4-Pz8_Pj4-Pz8_", "Websafe encoding broken"); + var dec = b.stringifyArray(b.decodeString(enc, true /* websafe */)); + G_Assert(z, dec === test, "Websafe dencoding broken"); + + // Test parsing malformed characters + var caught = false; + try { + b.decodeString("foooooo+oooo", true /*websafe*/); + } catch(e) { + caught = true; + } + G_Assert(z, caught, "Didn't throw on malformed input"); + + G_Debug(z, "PASSED"); + + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/cryptohasher.js b/mozilla/toolkit/components/protection/content/moz/cryptohasher.js new file mode 100644 index 00000000000..79c9e6ddd0b --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/cryptohasher.js @@ -0,0 +1,177 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// A very thin wrapper around nsICryptoHash. It's not strictly +// necessary, but makes the code a bit cleaner and gives us the +// opportunity to verify that our implementations give the results that +// we expect, for example if we have to interoperate with a server. +// +// The digest* methods reset the state of the hasher, so it's +// necessary to call init() explicitly after them. +// +// Works only in Firefox 1.5+. +// +// IMPORTANT NOTE: Due to https://bugzilla.mozilla.org/show_bug.cgi?id=321024 +// you cannot use the cryptohasher before app-startup. The symptom of doing +// so is a segfault in NSS. + +/** + * Instantiate a new hasher. You must explicitly call init() before use! + */ +function G_CryptoHasher() { + this.debugZone = "cryptohasher"; + this.hasher_ = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + this.initialized_ = false; +} + +G_CryptoHasher.algorithms = { + MD2: Ci.nsICryptoHash.MD2, + MD5: Ci.nsICryptoHash.MD5, + SHA1: Ci.nsICryptoHash.SHA1, + SHA256: Ci.nsICryptoHash.SHA256, + SHA384: Ci.nsICryptoHash.SHA384, + SHA512: Ci.nsICryptoHash.SHA512, +}; + +/** + * Initialize the hasher. This function must be called after every call + * to one of the digest* methods. + * + * @param algorithm Constant from G_CryptoHasher.algorithms specifying the + * algorithm this hasher will use + */ +G_CryptoHasher.prototype.init = function(algorithm) { + var validAlgorithm = false; + for (var alg in G_CryptoHasher.algorithms) + if (algorithm == G_CryptoHasher.algorithms[alg]) + validAlgorithm = true; + + if (!validAlgorithm) + throw new Error("Invalid algorithm: " + algorithm); + + this.initialized_ = true; + this.hasher_.init(algorithm); +} + +/** + * Update the hash's internal state with input given in a string. Can be + * called multiple times for incrementeal hash updates. + * + * @param input String containing data to hash. + */ +G_CryptoHasher.prototype.updateFromString = function(input) { + if (!this.initialized_) + throw new Error("You must initialize the hasher first!"); + this.hasher_.update(input.split(""), input.length); +} + +/** + * Update the hash's internal state with input given in an array. Can be + * called multiple times for incrementeal hash updates. + * + * @param input Array containing data to hash. + */ +G_CryptoHasher.prototype.updateFromArray = function(input) { + if (!this.initialized_) + throw new Error("You must initialize the hasher first!"); + this.hasher_.update(input, input.length); +} + +/** + * @returns The hash value as a string (sequence of 8-bit values) + */ +G_CryptoHasher.prototype.digestRaw = function() { + return this.hasher_.finish(false /* not b64 encoded */); +} + +/** + * @returns The hash value as a base64-encoded string + */ +G_CryptoHasher.prototype.digestBase64 = function() { + return this.hasher_.finish(true /* b64 encoded */); +} + +/** + * @returns The hash value as a hex-encoded string + */ +G_CryptoHasher.prototype.digestHex = function() { + var raw = this.digestRaw(); + return this.toHex_(raw); +} + +/** + * Converts a sequence of values to a hex-encoded string. The input is a + * a string, so you can stick 16-bit values in each character. + * + * @param str String to conver to hex. (Often this is just a sequence of + * 16-bit values) + * + * @returns String containing the hex representation of the input + */ +G_CryptoHasher.prototype.toHex_ = function(str) { + var hexchars = '0123456789ABCDEF'; + var hexrep = new Array(str.length * 2); + + for (var i = 0; i < str.length; ++i) { + hexrep[i * 2] = hexchars.charAt((str.charCodeAt(i) >> 4) & 15); + hexrep[i * 2 + 1] = hexchars.charAt(str.charCodeAt(i) & 15); + } + return hexrep.join(''); +} + +/** + * Lame unittest function + */ +function TEST_G_CryptoHasher() { + if (G_GDEBUG) { + var z = "cryptohasher UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + /* Add test vectors + * var hasher = new G_CryptoHasher(); + * hasher.updateFromtring("0"); + * hasher.updateFromtring("1"); + * etc + * var digest = hasher.digestHex(); + */ + + G_Debug(z, "PASSED"); + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/debug.js b/mozilla/toolkit/components/protection/content/moz/debug.js new file mode 100644 index 00000000000..92915f26dcf --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/debug.js @@ -0,0 +1,879 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * Annie Sullivan + * Aaron Boodman + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// Generic logging/debugging functionality that: +// +// (*) when disabled compiles to no-ops at worst (for calls to the service) +// and to nothing at best (calls to G_Debug() and similar are compiled +// away when you use a jscompiler that strips dead code) +// +// (*) has dynamically configurable/creatable debugging "zones" enabling +// selective logging +// +// (*) hides its plumbing so that all calls in different zones are uniform, +// so you can drop files using this library into other apps that use it +// without any configuration +// +// (*) can be controlled programmatically or via preferences. The +// preferences that control the service and its zones are under +// the preference branch "safebrowsing-debug-service." +// +// (*) outputs function call traces when the "loggifier" zone is enabled +// +// (*) can write output to logfiles so that you can get a call trace +// from someone who is having a problem +// +// Example: +// +// var G_GDEBUG = true // Enable this module +// var G_debugService = new G_DebugService(); // in global context +// +// // You can use it with arbitrary primitive first arguement +// G_Debug("myzone", "Yo yo yo"); // outputs: [myzone] Yo yo yo\n +// +// // But it's nice to use it with an object; it will probe for the zone name +// function Obj() { +// this.debugZone = "someobj"; +// } +// Obj.prototype.foo = function() { +// G_Debug(this, "foo called"); +// } +// (new Obj).foo(); // outputs: [someobj] foo called\n +// +// G_debugService.loggifier.loggify(Obj.prototype); // enable call tracing +// +// // En/disable specific zones programmatically (you can also use preferences) +// G_debugService.enableZone("somezone"); +// G_debugService.disableZone("someotherzone"); +// G_debugService.enableAllZones(); +// +// // We also have asserts and errors: +// G_Error(this, "Some error occurred"); // will throw +// G_Assert(this, (x > 3), "x not greater than three!"); // will throw +// +// See classes below for more methods. +// +// TODO add abililty to alert() instead of dump()? Should be easy. +// TODO add code to set prefs when not found to the default value of a tristate +// TODO add error level support +// TODO add ability to turn off console output +// +// -------> TO START DEBUGGING: set G_GDEBUG to true + +// These are the functions code will typically call. Everything is +// wrapped in if's so we can compile it away when G_GDEBUG is false. + + +if (!isDef(G_GDEBUG)) { + throw new Error("G_GDEBUG constant must be set before loading debug.js"); +} + + +/** + * Write out a debugging message. + * + * @param who The thingy to convert into a zone name corresponding to the + * zone to which this message belongs + * @param msg Message to output + */ +function G_Debug(who, msg) { + if (G_GDEBUG) { + G_GetDebugZone(who).debug(msg); + } +} + +/** + * Debugs loudly + */ +function G_DebugL(who, msg) { + if (G_GDEBUG) { + var zone = G_GetDebugZone(who); + + if (zone.zoneIsEnabled()) { + G_debugService.dump( + G_File.LINE_END_CHAR + + "************************************************************" + + G_File.LINE_END_CHAR); + + G_Debug(who, msg); + + G_debugService.dump( + "************************************************************" + + G_File.LINE_END_CHAR + + G_File.LINE_END_CHAR); + } + } +} + +/** + * Write out a call tracing message + * + * @param who The thingy to convert into a zone name corresponding to the + * zone to which this message belongs + * @param msg Message to output + */ +function G_TraceCall(who, msg) { + if (G_GDEBUG) { + if (G_debugService.callTracingEnabled()) { + G_debugService.dump(msg + G_File.LINE_END_CHAR); + } + } +} + +/** + * Write out an error (and throw) + * + * @param who The thingy to convert into a zone name corresponding to the + * zone to which this message belongs + * @param msg Message to output + */ +function G_Error(who, msg) { + if (G_GDEBUG) { + G_GetDebugZone(who).error(msg); + } +} + +/** + * Assert something as true and signal an error if it's not + * + * @param who The thingy to convert into a zone name corresponding to the + * zone to which this message belongs + * @param condition Boolean condition to test + * @param msg Message to output + */ +function G_Assert(who, condition, msg) { + if (G_GDEBUG) { + G_GetDebugZone(who).assert(condition, msg); + } +} + +/** + * Assert two things are equal (as in ==). + */ +function G_AssertEqual(who, expected, actual, msg) { + if (G_GDEBUG) { + G_GetDebugZone(who).assert( + expected == actual, + msg + " Expected: {%s}, got: {%s}".subs(expected, actual)); + } +} + +/** + * Helper function that takes input and returns the DebugZone + * corresponding to it. + * + * @param who Arbitrary input that will be converted into a zone name. Most + * likely an object that has .debugZone property, or a string. + * @returns The DebugZone object corresponding to the input + */ +function G_GetDebugZone(who) { + if (G_GDEBUG) { + var zone = "?"; + + if (who && who.debugZone) { + zone = who.debugZone; + } else if (isString(who)) { + zone = who; + } + + return G_debugService.getZone(zone); + } +} + +// Classes that implement the functionality. + +/** + * A debug "zone" is a string derived from arbitrary types (but + * typically derived from another string or an object). All debugging + * messages using a particular zone can be enabled or disabled + * independent of other zones. This enables you to turn on/off logging + * of particular objects or modules. This object implements a single + * zone and the methods required to use it. + * + * @constructor + * @param service Reference to the DebugService object we use for + * registration + * @param prefix String indicating the unique prefix we should use + * when creating preferences to control this zone + * @param zone String indicating the name of the zone + */ +function G_DebugZone(service, prefix, zone) { + if (G_GDEBUG) { + this.debugService_ = service; + this.prefix_ = prefix; + this.zone_ = zone; + this.zoneEnabledPrefName_ = prefix + ".zone." + this.zone_; + this.settings_ = new G_DebugSettings(); + } +} + +/** + * @returns Boolean indicating if this zone is enabled + */ +G_DebugZone.prototype.zoneIsEnabled = function() { + if (G_GDEBUG) { + var explicit = this.settings_.getSetting(this.zoneEnabledPrefName_, null); + + if (explicit !== null) { + return explicit; + } else { + return this.debugService_.allZonesEnabled(); + } + } +} + +/** + * Enable this logging zone + */ +G_DebugZone.prototype.enableZone = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.zoneEnabledPrefName_, true); + } +} + +/** + * Disable this logging zone + */ +G_DebugZone.prototype.disableZone = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.zoneEnabledPrefName_, false); + } +} + +/** + * Write a debugging message to this zone + * + * @param msg String of message to write + */ +G_DebugZone.prototype.debug = function(msg) { + if (G_GDEBUG) { + if (this.zoneIsEnabled()) { + this.debugService_.dump("[%s] %s%s".subs(this.zone_, + msg, + G_File.LINE_END_CHAR)); + } + } +} + +/** + * Write an error to this zone and throw + * + * @param msg String of error to write + */ +G_DebugZone.prototype.error = function(msg) { + if (G_GDEBUG) { + this.debugService_.dump("[%s] %s%s".subs(this.zone_, + msg, + G_File.LINE_END_CHAR)); + throw new Error(msg); + debugger; + } +} + +/** + * Assert something as true and error if it is not + * + * @param condition Boolean condition to test + * @param msg String of message to write if is false + */ +G_DebugZone.prototype.assert = function(condition, msg) { + if (G_GDEBUG) { + if (condition !== true) { + G_Error(this.zone_, "ASSERT FAILED: " + msg); + } + } +} + + +/** + * The debug service handles auto-registration of zones, namespacing + * the zones preferences, and various global settings such as whether + * all zones are enabled. + * + * @constructor + * @param opt_prefix Optional string indicating the unique prefix we should + * use when creating preferences + */ +function G_DebugService(opt_prefix) { + if (G_GDEBUG) { + this.prefix_ = opt_prefix ? opt_prefix : "safebrowsing-debug-service"; + this.consoleEnabledPrefName_ = this.prefix_ + ".alsologtoconsole"; + this.allZonesEnabledPrefName_ = this.prefix_ + ".enableallzones"; + this.callTracingEnabledPrefName_ = this.prefix_ + ".trace-function-calls"; + this.logFileEnabledPrefName_ = this.prefix_ + ".logfileenabled"; + this.logFileErrorLevelPrefName_ = this.prefix_ + ".logfile-errorlevel"; + this.zones_ = {}; + + this.loggifier = new G_Loggifier(); + this.settings_ = new G_DebugSettings(); + + // We observe the console service so that we can echo errors that get + // reported there to the file log. + Cc["@mozilla.org/consoleservice;1"] + .getService(Ci.nsIConsoleService) + .registerListener(this); + } +} + +// Error levels for reporting console messages to the log. +G_DebugService.ERROR_LEVEL_INFO = "INFO"; +G_DebugService.ERROR_LEVEL_WARNING = "WARNING"; +G_DebugService.ERROR_LEVEL_EXCEPTION = "EXCEPTION"; + + +/** + * @returns Boolean indicating if we should send messages to the jsconsole + */ +G_DebugService.prototype.alsoDumpToConsole = function() { + if (G_GDEBUG) { + return this.settings_.getSetting(this.consoleEnabledPrefName_, false); + } +} + +/** + * @returns whether to log output to a file as well as the console. + */ +G_DebugService.prototype.logFileIsEnabled = function() { + if (G_GDEBUG) { + return this.settings_.getSetting(this.logFileEnabledPrefName_, false); + } +} + +/** + * Turns on file logging. dump() output will also go to the file specified by + * setLogFile() + */ +G_DebugService.prototype.enableLogFile = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.logFileEnabledPrefName_, true); + } +} + +/** + * Turns off file logging + */ +G_DebugService.prototype.disableLogFile = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.logFileEnabledPrefName_, false); + } +} + +/** + * @returns an nsIFile instance pointing to the current log file location + */ +G_DebugService.prototype.getLogFile = function() { + if (G_GDEBUG) { + return this.logFile_; + } +} + +/** + * Sets a new log file location + */ +G_DebugService.prototype.setLogFile = function(file) { + if (G_GDEBUG) { + this.logFile_ = file; + } +} + +/** + * Enables sending messages to the jsconsole + */ +G_DebugService.prototype.enableDumpToConsole = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.consoleEnabledPrefName_, true); + } +} + +/** + * Disables sending messages to the jsconsole + */ +G_DebugService.prototype.disableDumpToConsole = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.consoleEnabledPrefName_, false); + } +} + +/** + * @param zone Name of the zone to get + * @returns The DebugZone object corresopnding to input. If not such + * zone exists, a new one is created and returned + */ +G_DebugService.prototype.getZone = function(zone) { + if (G_GDEBUG) { + if (!this.zones_[zone]) + this.zones_[zone] = new G_DebugZone(this, this.prefix_, zone); + + return this.zones_[zone]; + } +} + +/** + * @param zone Zone to enable debugging for + */ +G_DebugService.prototype.enableZone = function(zone) { + if (G_GDEBUG) { + var toEnable = this.getZone(zone); + toEnable.enableZone(); + } +} + +/** + * @param zone Zone to disable debugging for + */ +G_DebugService.prototype.disableZone = function(zone) { + if (G_GDEBUG) { + var toDisable = this.getZone(zone); + toDisable.disableZone(); + } +} + +/** + * @returns Boolean indicating whether debugging is enabled for all zones + */ +G_DebugService.prototype.allZonesEnabled = function() { + if (G_GDEBUG) { + return this.settings_.getSetting(this.allZonesEnabledPrefName_, false); + } +} + +/** + * Enables all debugging zones + */ +G_DebugService.prototype.enableAllZones = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.allZonesEnabledPrefName_, true); + } +} + +/** + * Disables all debugging zones + */ +G_DebugService.prototype.disableAllZones = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.allZonesEnabledPrefName_, false); + } +} + +/** + * @returns Boolean indicating whether call tracing is enabled + */ +G_DebugService.prototype.callTracingEnabled = function() { + if (G_GDEBUG) { + return this.settings_.getSetting(this.callTracingEnabledPrefName_, false); + } +} + +/** + * Enables call tracing + */ +G_DebugService.prototype.enableCallTracing = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.callTracingEnabledPrefName_, true); + } +} + +/** + * Disables call tracing + */ +G_DebugService.prototype.disableCallTracing = function() { + if (G_GDEBUG) { + this.settings_.setDefault(this.callTracingEnabledPrefName_, false); + } +} + +/** + * Gets the minimum error that will be reported to the log. + */ +G_DebugService.prototype.getLogFileErrorLevel = function() { + if (G_GDEBUG) { + var level = this.settings_.getSetting(this.logFileErrorLevelPrefName_, + G_DebugService.ERROR_LEVEL_EXCEPTION); + + return level.toUpperCase(); + } +} + +/** + * Sets the minimum error level that will be reported to the log. + */ +G_DebugService.prototype.setLogFileErrorLevel = function(level) { + if (G_GDEBUG) { + // normalize case just to make it slightly easier to not screw up. + level = level.toUpperCase(); + + if (level != G_DebugService.ERROR_LEVEL_INFO && + level != G_DebugService.ERROR_LEVEL_WARNING && + level != G_DebugService.ERROR_LEVEL_EXCEPTION) { + throw new Error("Invalid error level specified: {" + level + "}"); + } + + this.settings_.setDefault(this.logFileErrorLevelPrefName_, level); + } +} + +/** + * Internal dump() method + * + * @param msg String of message to dump + */ +G_DebugService.prototype.dump = function(msg) { + if (G_GDEBUG) { + dump(msg); + + if (this.alsoDumpToConsole()) { + try { + var console = Components.classes['@mozilla.org/consoleservice;1'] + .getService(Components.interfaces.nsIConsoleService); + console.logStringMessage(msg); + } catch(e) { + dump("G_DebugZone ERROR: COULD NOT DUMP TO CONSOLE" + + G_File.LINE_END_CHAR); + } + } + + this.maybeDumpToFile(msg); + } +} + +/** + * Writes the specified message to the log file, if file logging is enabled. + */ +G_DebugService.prototype.maybeDumpToFile = function(msg) { + if (this.logFileIsEnabled() && this.logFile_) { + if (!this.logWriter_) { + this.logWriter_ = new G_FileWriter(this.logFile_, true); + } + + this.logWriter_.write(msg); + } +} + +/** + * Implements nsIConsoleListener.observe(). Gets called when an error message + * gets reported to the console and sends it to the log file as well. + */ +G_DebugService.prototype.observe = function(consoleMessage) { + if (G_GDEBUG) { + var errorLevel = this.getLogFileErrorLevel(); + + // consoleMessage can be either nsIScriptError or nsIConsoleMessage. The + // latter does not have things like line number, etc. So we special case + // it first. + if (!(consoleMessage instanceof Ci.nsIScriptError)) { + // Only report these messages if the error level is INFO. + if (errorLevel == G_DebugService.ERROR_LEVEL_INFO) { + this.maybeDumpToFile(G_DebugService.ERROR_LEVEL_INFO + ": " + + consoleMessage.message + G_File.LINE_END_CHAR); + } + + return; + } + + // We make a local copy of these fields because writing to it doesn't seem + // to work. + var flags = consoleMessage.flags; + var sourceName = consoleMessage.sourceName; + var lineNumber = consoleMessage.lineNumber; + + // Sometimes, a scripterror instance won't have any flags set. We + // default to exception. + if (!flags) { + flags = Ci.nsIScriptError.exceptionFlag; + } + + // Default the filename and line number if they aren't set. + if (!sourceName) { + sourceName = ""; + } + + if (!lineNumber) { + lineNumber = ""; + } + + // Report the error in the log file. + if (flags & Ci.nsIScriptError.warningFlag) { + // Only report warnings if the error level is warning or better. + if (errorLevel == G_DebugService.ERROR_LEVEL_WARNING || + errorLevel == G_DebugService.ERROR_LEVEL_INFO) { + this.reportScriptError_(consoleMessage.message, + sourceName, + lineNumber, + G_DebugService.ERROR_LEVEL_WARNING); + } + } else if (flags & Ci.nsIScriptError.exceptionFlag) { + // Always report exceptions. + this.reportScriptError_(consoleMessage.message, + sourceName, + lineNumber, + G_DebugService.ERROR_LEVEL_EXCEPTION); + } + } +} + +/** + * Private helper to report an nsIScriptError instance to the log/console. + */ +G_DebugService.prototype.reportScriptError_ = function(message, sourceName, + lineNumber, label) { + var message = ["", + "------------------------------------------------------------", + label + ": " + message, + "location: " + sourceName + ", " + "line: " + lineNumber, + "------------------------------------------------------------", + "", + ""].join(G_File.LINE_END_CHAR); + + dump(message); + this.maybeDumpToFile(message); +} + + + +/** + * A class that instruments methods so they output a call trace, + * including the values of their actual parameters and return value. + * This code is mostly stolen from Aaron Boodman's original + * implementation in clobber utils. + * + * Note that this class uses the "loggifier" debug zone, so you'll see + * a complete call trace when that zone is enabled. + * + * @constructor + */ +function G_Loggifier() { + if (G_GDEBUG) { + // Careful not to loggify ourselves! + this.mark_(this); + } +} + +/** + * Marks an object as having been loggified. Loggification is not + * idempotent :) + * + * @param obj Object to be marked + */ +G_Loggifier.prototype.mark_ = function(obj) { + if (G_GDEBUG) { + obj.__loggified_ = true; + } +} + +/** + * @param obj Object to be examined + * @returns Boolean indicating if the object has been loggified + */ +G_Loggifier.prototype.isLoggified = function(obj) { + if (G_GDEBUG) { + return !!obj.__loggified_; + } +} + +/** + * Attempt to extract the class name from the constructor definition. + * Assumes the object was created using new. + * + * @param constructor String containing the definition of a constructor, + * for example what you'd get by examining obj.constructor + * @returns Name of the constructor/object if it could be found, else "???" + */ +G_Loggifier.prototype.getFunctionName_ = function(constructor) { + if (G_GDEBUG) { + return constructor.name || "???"; + } +} + +/** + * Wraps all the methods in an object so that call traces are + * automatically outputted. + * + * @param obj Object to loggify. SHOULD BE THE PROTOTYPE OF A USER-DEFINED + * object. You can get into trouble if you attempt to + * loggify something that isn't, for example the Window. + * + * Any additional parameters are considered method names which should not be + * loggified. + * + * Usage: + * G_debugService.loggifier.loggify(MyClass.prototype, + * "firstMethodNotToLog", + * "secondMethodNotToLog", + * ... etc ...); + */ +G_Loggifier.prototype.loggify = function(obj) { + if (G_GDEBUG) { + if (!G_debugService.callTracingEnabled()) { + return; + } + + if (typeof window != "undefined" && obj == window || + this.isLoggified(obj)) // Don't go berserk! + return; + + var zone = G_GetDebugZone(obj); + if (!zone || !zone.zoneIsEnabled()) { + return; + } + + this.mark_(obj); + + // Helper function returns an instrumented version of + // objName.meth, with "this" bound properly. (BTW, because we're + // in a conditional here, functions will only be defined as + // they're encountered during execution, so declare this helper + // before using it.) + + function wrap(meth, objName, methName) { + return function() { + + // First output the call along with actual parameters + var args = new Array(arguments.length); + var argsString = ""; + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + argsString += (i == 0 ? "" : ", "); + + if (isFunction(args[i])) { + argsString += "[function]"; + } else { + argsString += args[i]; + } + } + + G_TraceCall(this, "> " + objName + "." + methName + "(" + + argsString + ")"); + + // Then run the function, capturing the return value and throws + try { + var retVal = meth.apply(this, arguments); + var reportedRetVal = retVal; + + if (typeof reportedRetVal == "undefined") + reportedRetVal = "void"; + else if (reportedRetVal === "") + reportedRetVal = "\"\" (empty string)"; + } catch (e) { + if (e && !e.__logged) { + G_TraceCall(this, "Error: " + e.message + ". " + + e.fileName + ": " + e.lineNumber); + try { + e.__logged = true; + } catch (e2) { + // Sometimes we can't add the __logged flag because it's an + // XPC wrapper + throw e; + } + } + + throw e; // Re-throw! + } + + // And spit it out already + G_TraceCall( + this, + "< " + objName + "." + methName + ": " + reportedRetVal); + + return retVal; + }; + }; + + var ignoreLookup = {}; + + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + ignoreLookup[arguments[i]] = true; + } + } + + // Wrap each method of obj + for (var p in obj) { + // Work around bug in Firefox. In ffox typeof RegExp is "function", + // so make sure this really is a function. Bug as of FFox 1.5b2. + if (typeof obj[p] == "function" && obj[p].call && !ignoreLookup[p]) { + var objName = this.getFunctionName_(obj.constructor); + obj[p] = wrap(obj[p], objName, p); + } + } + } +} + + +/** + * Simple abstraction around debug settings. The thing with debug settings is + * that we want to be able to specify a default in the application's startup, + * but have that default be overridable by the user via their prefs. + * + * To generalize this, we package up a dictionary of defaults with the + * preferences tree. If a setting isn't in the preferences tree, then we grab it + * from the defaults. + */ +function G_DebugSettings() { + this.defaults_ = {}; + this.prefs_ = new G_Preferences(); +} + +/** + * Returns the value of a settings, optionally defaulting to a given value if it + * doesn't exist. If no default is specified, the default is |undefined|. + */ +G_DebugSettings.prototype.getSetting = function(name, opt_default) { + var override = this.prefs_.getPref(name, null); + + if (override !== null) { + return override; + } else if (isDef(this.defaults_[name])) { + return this.defaults_[name]; + } else { + return opt_default; + } +} + +/** + * Sets the default value for a setting. If the user doesn't override it with a + * preference, this is the value which will be returned by getSetting(). + */ +G_DebugSettings.prototype.setDefault = function(name, val) { + this.defaults_[name] = val; +} + +var G_debugService = new G_DebugService(); // Instantiate us! + +if (G_GDEBUG) { + G_debugService.enableAllZones(); +} diff --git a/mozilla/toolkit/components/protection/content/moz/filesystem.js b/mozilla/toolkit/components/protection/content/moz/filesystem.js new file mode 100644 index 00000000000..b5df5228676 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/filesystem.js @@ -0,0 +1,307 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Aaron Boodman (original author) + * Raphael Moll + * Fritz Schneider + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// Utilities for working with nsIFile and related interfaces. + +/** + * Stub for an nsIFile wrapper which doesn't exist yet. Perhaps in the future + * we could add functionality to nsILocalFile which would be useful to us here, + * but for now, no need for such. This could be done by setting + * __proto__ to an instance of nsIFile, for example. Neat. + */ +var G_File = {}; + +/** + * Returns an nsIFile pointing to the user's home directory, or optionally, a + * file inside that dir. + */ +G_File.getHomeFile = function(opt_file) { + return this.getSpecialFile("Home", opt_file); +} + +/** + * Returns an nsIFile pointing to the current profile folder, or optionally, a + * file inside that dir. + */ +G_File.getProfileFile = function(opt_file) { + return this.getSpecialFile("ProfD", opt_file); +} + +/** + * returns an nsIFile pointing to the temporary dir, or optionally, a file + * inside that dir. + */ +G_File.getTempFile = function(opt_file) { + return this.getSpecialFile("TmpD", opt_file); +} + +/** + * Returns an nsIFile pointing to one of the special named directories defined + * by Firefox, such as the user's home directory, the profile directory, etc. + * + * As a convenience, callers may specify the opt_file argument to get that file + * within the special directory instead. + * + * http://lxr.mozilla.org/seamonkey/source/xpcom/io/nsDirectoryServiceDefs.h + * http://kb.mozillazine.org/File_IO#Getting_special_files + */ +G_File.getSpecialFile = function(loc, opt_file) { + var file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get(loc, Ci.nsILocalFile); + + if (opt_file) { + file.append(opt_file); + } + + return file; +} + +/** + * Creates and returns a pointer to a unique file in the temporary directory + * with an optional base name. + */ +G_File.createUniqueTempFile = function(opt_baseName) { + var baseName = (opt_baseName || (new Date().getTime())) + ".tmp"; + + var file = this.getSpecialFile("TmpD", baseName); + file.createUnique(file.NORMAL_FILE_TYPE, 0644); + + return file; +} + +/** + * Creates and returns a pointer to a unique temporary directory, with + * an optional base name. + */ +G_File.createUniqueTempDir = function(opt_baseName) { + var baseName = (opt_baseName || (new Date().getTime())) + ".tmp"; + + var dir = this.getSpecialFile("TmpD", baseName); + dir.createUnique(dir.DIRECTORY_TYPE, 0744); + + return dir; +} + +/** + * Static method to retrieve an nsIFile from a file:// URI. + */ +G_File.fromFileURI = function(uri) { + // Ensure they use file:// url's: discourages platform-specific paths + if (uri.indexOf("file://") != 0) + throw new Error("File path must be a file:// URL"); + + var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"] + .createInstance(Ci.nsIFileProtocolHandler); + return fileHandler.getFileFromURLSpec(uri); +} + +// IO Constants + +G_File.PR_RDONLY = 0x01; // read-only +G_File.PR_WRONLY = 0x02; // write only +G_File.PR_RDWR = 0x04; // reading and writing +G_File.PR_CREATE_FILE = 0x08; // create if it doesn't exist +G_File.PR_APPEND = 0x10; // file pntr reset to end prior to writes +G_File.PR_TRUNCATE = 0x20; // file exists its length is set to zero +G_File.PR_SYNC = 0x40; // writes wait for data to be physically written +G_File.PR_EXCL = 0x80; // file does not exist ? created : no action + +// The character(s) to use for line-endings, which are platform-specific. +// This doesn't work for mac os9, but I don't know of a good way to detect +// OS9-ness from JS. +G_File.LINE_END_CHAR = + Cc["@mozilla.org/network/protocol;1?name=https"] + .getService(Ci.nsIHttpProtocolHandler) + .platform.toLowerCase().indexOf("win") == 0 ? "\r\n" : "\n"; + +/** + * A class which can read a file incrementally or all at once. Parameter can be + * either an nsIFile instance or a string file:// URI. + * Note that this class is not compatible with non-ascii data. + */ +function G_FileReader(file) { + this.file_ = isString(file) ? G_File.fromFileURI(file) : file; +} + +/** + * Utility method to read the entire contents of a file. Parameter can be either + * an nsIFile instance or a string file:// URI. + */ +G_FileReader.readAll = function(file) { + var reader = new G_FileReader(file); + + try { + return reader.read(); + } finally { + reader.close(); + } +} + +/** + * Read up to opt_maxBytes from the stream. If opt_maxBytes is not specified, + * the entire file is read. + */ +G_FileReader.prototype.read = function(opt_maxBytes) { + if (!this.stream_) { + var fs = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + fs.init(this.file_, G_File.PR_RDONLY, 0444, 0); + + this.stream_ = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + this.stream_.init(fs); + } + + if (!isDef(opt_maxBytes)) { + opt_maxBytes = this.stream_.available(); + } + + return this.stream_.read(opt_maxBytes); +} + +/** + * Close the stream. This step is required when reading is done. + */ +G_FileReader.prototype.close = function(opt_maxBytes) { + if (this.stream_) { + this.stream_.close(); + this.stream_ = null; + } +} + + +// TODO(anyone): Implement G_LineReader. The interface should be something like: +// for (var line = null; line = reader.readLine();) { +// // do something with line +// } + + +/** + * Writes a file incrementally or all at once. + * Note that this class is not compatible with non-ascii data. + */ +function G_FileWriter(file, opt_append) { + this.file_ = typeof file == "string" ? G_File.fromFileURI(file) : file; + this.append_ = !!opt_append; +} + +/** + * Helper to write to a file in one step. + */ +G_FileWriter.writeAll = function(file, data, opt_append) { + var writer = new G_FileWriter(file, opt_append); + + try { + return writer.write(data); + } finally { + writer.close(); + return 0; + } +} + +/** + * Write bytes out to the file. Returns the number of bytes written. + */ +G_FileWriter.prototype.write = function(data) { + if (!this.stream_) { + this.stream_ = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + + var flags = G_File.PR_WRONLY | + G_File.PR_CREATE_FILE | + (this.append_ ? G_File.PR_APPEND : G_File.PR_TRUNCATE); + + + this.stream_.init(this.file_, + flags, + -1 /* default perms */, + 0 /* no special behavior */); + } + + return this.stream_.write(data, data.length); +} + +/** + * Writes bytes out to file followed by the approriate line-ending character for + * the current platform. + */ +G_FileWriter.prototype.writeLine = function(data) { + this.write(data + G_File.LINE_END_CHAR); +} + + +/** + * Closes the file. This must becalled when writing is done. + */ +G_FileWriter.prototype.close = function() { + if (this.stream_) { + this.stream_.close(); + this.stream_ = null; + } +} + + +function TEST_G_File() { + if (G_GDEBUG) { + var z = "gfile UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + // Test G_File.createUniqueTempir + try { + var dir = G_File.createUniqueTempDir(); + } catch(e) { + G_Debug(z, e); + G_Assert(z, false, "Failed to create temp dir"); + } + + G_Assert(z, dir.exists(), "Temp dir doesn't exist: " + dir.path); + G_Assert(z, dir.isDirectory(), "Temp dir isn't a directory: " + dir.path); + G_Assert(z, dir.isReadable(), "Temp dir isn't readable: " + dir.path); + G_Assert(z, dir.isWritable(), "Temp dir isn't writable: " + dir.path); + + dir.remove(true /* recurse */); + + // TODO: WE NEED MORE UNITTESTS! + + G_Debug(z, "PASS"); + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/lang.js b/mozilla/toolkit/components/protection/content/moz/lang.js new file mode 100644 index 00000000000..17913b2d319 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/lang.js @@ -0,0 +1,101 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Aaron Boodman (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// Firefox-specific additions to lib/js/lang.js. + + +/** + * The always-useful alert. + */ +function alert(msg, opt_title) { + opt_title |= "message"; + + Cc["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Ci.nsIPromptService) + .alert(null, opt_title, msg.toString()); +} + + +/** + * The instanceof operator cannot be used on a pure js object to determine if + * it implements a certain xpcom interface. The QueryInterface method can, but + * it throws an error which makes things more complex. + */ +function jsInstanceOf(obj, iid) { + try { + obj.QueryInterface(iid); + return true; + } catch (e) { + if (e == Components.results.NS_ERROR_NO_INTERFACE) { + return false; + } else { + throw e; + } + } +} + + +/** + * Unbelievably, Function inheritence is broken in chrome in Firefox + * (still as of FFox 1.5b1). Hence if you're working in an extension + * and not using the subscriptloader, you can't use the method + * above. Instead, use this global function that does roughly the same + * thing. + * + *************************************************************************** + * NOTE THE REVERSED ORDER OF FUNCTION AND OBJECT REFERENCES AS bind() * + *************************************************************************** + * + * // Example to bind foo.bar(): + * var bound = BindToObject(bar, foo, "arg1", "arg2"); + * bound("arg3", "arg4"); + * + * @param func {string} Reference to the function to be bound + * + * @param obj {object} Specifies the object which |this| should point to + * when the function is run. If the value is null or undefined, it will default + * to the global object. + * + * @param opt_{...} Dummy optional arguments to make a jscompiler happy + * + * @returns {function} A partially-applied form of the speficied function. + */ +function BindToObject(func, obj, opt_A, opt_B, opt_C, opt_D, opt_E, opt_F) { + // This is the sick product of Aaron's mind. Not for the feint of heart. + var args = Array.prototype.splice.call(arguments, 1, arguments.length); + return Function.prototype.bind.apply(func, args); +} diff --git a/mozilla/toolkit/components/protection/content/moz/objectsafemap.js b/mozilla/toolkit/components/protection/content/moz/objectsafemap.js new file mode 100644 index 00000000000..7593fa7008a --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/objectsafemap.js @@ -0,0 +1,229 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// ObjectSafeMap is, shockingly, a Map with which it is safe to use +// objects as keys. It currently uses parallel arrays for storage, +// rendering it inefficient (linear) for large maps. We can always +// swap out the implementation if this becomes a problem. Note that +// this class uses strict equality to determine equivalent keys. +// +// Interface: +// +// insert(key, value) +// erase(key) // Returns true if key erased, false if not found +// find(key) // Returns undefined if key not found +// replace(otherMap) // Clones otherMap, replacing the current map +// forEach(func) +// size() // Returns number of items in the map +// +// TODO: should probably make it iterable by implementing getList(); + + +/** + * Create a new ObjectSafeMap. + * + * @param opt_name A string used to name the map + * + * @constructor + */ +function G_ObjectSafeMap(opt_name) { + this.debugZone = "objectsafemap"; + this.name_ = opt_name ? opt_name : "noname"; + this.keys_ = []; + this.values_ = []; +} + +/** + * Helper function to return the index of a key. + * + * @param key An key to find + * + * @returns Index in the keys array where the key is found, -1 if not + */ +G_ObjectSafeMap.prototype.indexOfKey_ = function(key) { + for (var i = 0; i < this.keys_.length; i++) + if (this.keys_[i] === key) + return i; + return -1; +} + +/** + * Add an item + * + * @param key An key to add (overwrites previous key) + * + * @param value The value to store at that key + */ +G_ObjectSafeMap.prototype.insert = function(key, value) { + if (key === null) + throw new Error("Can't use null as a key"); + if (value === undefined) + throw new Error("Can't store undefined values in this map"); + + var i = this.indexOfKey_(key); + if (i == -1) { + this.keys_.push(key); + this.values_.push(value); + } else { + this.keys_[i] = key; + this.values_[i] = value; + } + + G_Assert(this, this.keys_.length == this.values_.length, + "Different number of keys than values!"); +} + +/** + * Remove a key from the map + * + * @param key The key to remove + * + * @returns Boolean indicating if the key was removed + */ +G_ObjectSafeMap.prototype.erase = function(key) { + var keyLocation = this.indexOfKey_(key); + var keyFound = keyLocation != -1; + if (keyFound) { + this.keys_.splice(keyLocation, 1); + this.values_.splice(keyLocation, 1); + } + G_Assert(this, this.keys_.length == this.values_.length, + "Different number of keys than values!"); + + return keyFound; +} + +/** + * Look up a key in the map + * + * @param key The key to look up + * + * @returns The value at that key or undefined if it doesn't exist + */ +G_ObjectSafeMap.prototype.find = function(key) { + var keyLocation = this.indexOfKey_(key); + return keyLocation == -1 ? undefined : this.values_[keyLocation]; +} + +/** + * Replace one map with the content of another + * + * @param map input map that needs to be merged into our map + */ +G_ObjectSafeMap.prototype.replace = function(other) { + this.keys_ = []; + this.values_ = []; + for (var i = 0; i < other.keys_.length; i++) { + this.keys_.push(other.keys_[i]); + this.values_.push(other.values_[i]); + } + + G_Assert(this, this.keys_.length == this.values_.length, + "Different number of keys than values!"); +} + +/** + * Apply a function to each of the key value pairs. + * + * @param func Function to apply to the map's key value pairs + */ +G_ObjectSafeMap.prototype.forEach = function(func) { + if (typeof func != "function") + throw new Error("argument to forEach is not a function, it's a(n) " + + typeof func); + + for (var i = 0; i < this.keys_.length; i++) + func(this.keys_[i], this.values_[i]); +} + +/** + * @returns The number of keys in the map + */ +G_ObjectSafeMap.prototype.size = function() { + return this.keys_.length; +} + +// Cheesey, yes, but it's what we've got +function TEST_G_ObjectSafeMap() { + if (G_GDEBUG) { + var z = "map UNITTEST"; + G_debugService.enableZone(z); + G_Debug(z, "Starting"); + + var m = new G_ObjectSafeMap(); + G_Assert(z, m.size() == 0, "Initial size not zero"); + + var o1 = new Object; + var v1 = "1"; + var o2 = new Object; + var v2 = "1"; + + G_Assert(z, m.find(o1) == undefined, "Found non-existent item"); + + m.insert(o1, v1); + m.insert(o2, v2); + + G_Assert(z, m.size() == 2, "Size not 2"); + G_Assert(z, m.find(o1) == "1", "Didn't find item 1"); + G_Assert(z, m.find(o2) == "1", "Didn't find item 1"); + + m.insert(o1, "2"); + + G_Assert(z, m.size() == 2, "Size not 2"); + G_Assert(z, m.find(o1) == "2", "Didn't find item 1"); + G_Assert(z, m.find(o2) == "1", "Didn't find item 1"); + + m.erase(o1); + + G_Assert(z, m.size() == 1, "Size not 1"); + G_Assert(z, m.find(o1) == undefined, "Found item1"); + G_Assert(z, m.find(o2) == "1", "Didn't find item 2"); + + m.erase(o1); + + G_Assert(z, m.size() == 1, "Size not 1"); + G_Assert(z, m.find(o1) == undefined, "Found item1"); + G_Assert(z, m.find(o2) == "1", "Didn't find item 2"); + + m.erase(o2); + + G_Assert(z, m.size() == 0, "Size not 0"); + G_Assert(z, m.find(o2) == undefined, "Found item2"); + + G_Debug(z, "PASSED"); + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/observer.js b/mozilla/toolkit/components/protection/content/moz/observer.js new file mode 100644 index 00000000000..671bb29e4c7 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/observer.js @@ -0,0 +1,174 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// A couple of classes to simplify creating observers. +// +// // Example1: +// +// function doSomething() { ... } +// var observer = new G_ObserverWrapper(topic, doSomething); +// someObj.addObserver(topic, observer); +// +// // Example2: +// +// function doSomething() { ... } +// new G_ObserverServiceObserver("profile-after-change", +// doSomething, +// true /* run only once */); + + +/** + * This class abstracts the admittedly simple boilerplate required of + * an nsIObserver. It saves you the trouble of implementing the + * indirection of your own observe() function. + * + * @param topic String containing the topic the observer will filter for + * + * @param observeFunction Reference to the function to call when the + * observer fires + * + * @constructor + */ +function G_ObserverWrapper(topic, observeFunction) { + this.debugZone = "observer"; + this.topic_ = topic; + this.observeFunction_ = observeFunction; +} + +/** + * XPCOM + */ +G_ObserverWrapper.prototype.QueryInterface = function(iid) { + if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIObserver)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; +} + +/** + * Invoked by the thingy being observed + */ +G_ObserverWrapper.prototype.observe = function(subject, topic, data) { + if (topic == this.topic_) + this.observeFunction_(subject, topic, data); +} + + +/** + * This class abstracts the admittedly simple boilerplate required of + * observing an observerservice topic. It implements the indirection + * required, and automatically registers to hear the topic. + * + * @param topic String containing the topic the observer will filter for + * + * @param observeFunction Reference to the function to call when the + * observer fires + * + * @param opt_onlyOnce Boolean indicating if the observer should unregister + * after it has fired + * + * @constructor + */ +function G_ObserverServiceObserver(topic, observeFunction, opt_onlyOnce) { + this.debugZone = "observerserviceobserver"; + this.topic_ = topic; + this.observeFunction_ = observeFunction; + this.onlyOnce_ = !!opt_onlyOnce; + + this.observer_ = new G_ObserverWrapper(this.topic_, + BindToObject(this.observe_, this)); + this.observerService_ = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + this.observerService_.addObserver(this.observer_, this.topic_, false); +} + +/** + * Unregister the observer from the observerservice + */ +G_ObserverServiceObserver.prototype.unregister = function() { + this.observerService_.removeObserver(this.observer_, this.topic_); +} + +/** + * Invoked by the observerservice + */ +G_ObserverServiceObserver.prototype.observe_ = function(subject, topic, data) { + this.observeFunction_(subject, topic, data); + if (this.onlyOnce_) + this.unregister(); +} + + +function TEST_G_Observer() { + if (G_GDEBUG) { + + var z = "observer UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + var regularObserverRan = 0; + var observerServiceObserverRan = 0; + + function regularObserver() { + regularObserverRan++; + }; + + function observerServiceObserver() { + observerServiceObserverRan++; + }; + + var service = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + var topic = "google-observer-test"; + + var o1 = new G_ObserverWrapper(topic, regularObserver); + service.addObserver(o1, topic, false); + + new G_ObserverServiceObserver(topic, + observerServiceObserver, true /* once */); + + // Notifications happen synchronously, so this is easy + service.notifyObservers(null, topic, null); + service.notifyObservers(null, topic, null); + + G_Assert(z, regularObserverRan == 2, "Regular observer broken"); + G_Assert(z, observerServiceObserverRan == 1, "ObsServObs broken"); + + service.removeObserver(o1, topic); + G_Debug(z, "PASSED"); + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/preferences.js b/mozilla/toolkit/components/protection/content/moz/preferences.js new file mode 100644 index 00000000000..9df5687a5d6 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/preferences.js @@ -0,0 +1,361 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// Class for manipulating preferences. Aside from wrapping the pref +// service, useful functionality includes: +// +// - abstracting prefobserving so that you can observe preferences +// without implementing nsIObserver +// +// - getters that return a default value when the pref doesn't exist +// (instead of throwing) +// +// - get-and-set getters +// +// Example: +// +// var p = new PROT_Preferences(); +// alert(p.getPref("some-true-pref")); // shows true +// alert(p.getPref("no-such-pref", true)); // shows true +// alert(p.getPref("no-such-pref", null)); // shows null +// +// function observe(prefThatChanged) { +// alert("Pref changed: " + prefThatChanged); +// }; +// +// p.addObserver("somepref", observe); +// p.setPref("somepref", true); // alerts +// p.removeObserver("somepref", observe); +// +// TODO: should probably have the prefobserver pass in the new and old +// values + +// TODO(tc): Maybe remove this class and just call natively since we're no +// longer an extension. + +/** + * A class that wraps the preferences service. + * + * @param opt_startPoint A starting point on the prefs tree to resolve + * names passed to setPref and getPref. + * + * @param opt_useDefaultPranch Set to true to work against the default + * preferences tree instead of the profile one. + * + * @constructor + */ +function G_Preferences(opt_startPoint, opt_getDefaultBranch) { + this.debugZone = "prefs"; + this.observers_ = {}; + + var startPoint = opt_startPoint || null; + var prefSvc = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + + if (opt_getDefaultBranch) { + this.prefs_ = prefSvc.getDefaultBranch(startPoint); + } else { + this.prefs_ = prefSvc.getBranch(startPoint); + } + + // QI to prefinternal in case we want to add observers + this.prefs_.QueryInterface(Ci.nsIPrefBranchInternal); +} + +G_Preferences.setterMap_ = { "string": "setCharPref", + "boolean": "setBoolPref", + "number": "setIntPref" }; + +G_Preferences.getterMap_ = {}; +G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_STRING] = "getCharPref"; +G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_BOOL] = "getBoolPref"; +G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_INT] = "getIntPref"; + + +/** + * Stores a key/value in a user preference. Valid types for val are string, + * boolean, and number. Complex values are not yet supported (but feel free to + * add them!). + */ +G_Preferences.prototype.setPref = function(key, val) { + var datatype = typeof(val); + + if (datatype == "number" && (val % 1 != 0)) { + throw new Error("Cannot store non-integer numbers in preferences."); + } + + var meth = G_Preferences.setterMap_[datatype]; + + if (!meth) { + throw new Error("Pref datatype {" + datatype + "} not supported."); + } + + return this.prefs_[meth](key, val); +} + +/** + * Retrieves a user preference. Valid types for the value are the same as for + * setPref. If the preference is not found, opt_default will be returned + * instead. + */ +G_Preferences.prototype.getPref = function(key, opt_default) { + var type = this.prefs_.getPrefType(key); + + // zero means that the specified pref didn't exist + if (type == Ci.nsIPrefBranch.PREF_INVALID) { + return opt_default; + } + + var meth = G_Preferences.getterMap_[type]; + + if (!meth) { + throw new Error("Pref datatype {" + type + "} not supported."); + } + + // If a pref has been cleared, it will have a valid type but won't + // be gettable, so this will throw. + try { + return this.prefs_[meth](key); + } catch(e) { + return opt_default; + } +} + +/** + * Set a boolean preference + * + * @param which Name of preference to set + * @param value Boolean indicating value to set + * + * @deprecated Just use setPref. + */ +G_Preferences.prototype.setBoolPref = function(which, value) { + return this.setPref(which, value); +} + +/** + * Get a boolean preference. WILL THROW IF PREFERENCE DOES NOT EXIST. + * If you don't want this behavior, use getBoolPrefOrDefault. + * + * @param which Name of preference to get. + * + * @deprecated Just use getPref. + */ +G_Preferences.prototype.getBoolPref = function(which) { + return this.prefs_.getBoolPref(which); +} + +/** + * Get a boolean preference or return some default value if it doesn't + * exist. Note that the default doesn't have to be bool -- it could be + * anything (e.g., you could pass in null and check if the return + * value is === null to determine if the pref doesn't exist). + * + * @param which Name of preference to get. + * @param def Value to return if the preference doesn't exist + * @returns Boolean value of the pref if it exists, else def + * + * @deprecated Just use getPref. + */ +G_Preferences.prototype.getBoolPrefOrDefault = function(which, def) { + return this.getPref(which, def); +} + +/** + * Get a boolean preference if it exists. If it doesn't, set its value + * to a default and return the default. Note that the default will be + * coherced to a bool if it is set, but not in the return value. + * + * @param which Name of preference to get. + * @param def Value to set and return if the preference doesn't exist + * @returns Boolean value of the pref if it exists, else def + * + * @deprecated Just use getPref. + */ +G_Preferences.prototype.getBoolPrefOrDefaultAndSet = function(which, def) { + try { + return this.prefs_.getBoolPref(which); + } catch(e) { + this.prefs_.setBoolPref(which, !!def); // The !! forces boolean conversion + return def; + } +} + +/** + * Delete a preference. + * + * @param which Name of preference to obliterate + */ +G_Preferences.prototype.clearPref = function(which) { + try { + // This throws if the pref doesn't exist, which is fine because a + // non-existent pref is cleared + this.prefs_.clearUserPref(which); + } catch(e) {} +} + +/** + * Add an observer for a given pref. + * + * @param which String containing the pref to listen to + * @param callback Function to be called when the pref changes. This + * function will receive a single argument, a string + * holding the preference name that changed + */ +G_Preferences.prototype.addObserver = function(which, callback) { + var observer = new G_PreferenceObserver(callback); + // Need to store the observer we create so we can eventually unregister it + if (!this.observers_[which]) + this.observers_[which] = new G_ObjectSafeMap(); + this.observers_[which].insert(callback, observer); + this.prefs_.addObserver(which, observer, false /* strong reference */); +} + +/** + * Remove an observer for a given pref. + * + * @param which String containing the pref to stop listening to + * @param callback Function to remove as an observer + */ +G_Preferences.prototype.removeObserver = function(which, callback) { + var observer = this.observers_[which].find(callback); + G_Assert(this, !!observer, "Tried to unregister a nonexistant observer"); + this.prefs_.removeObserver(which, observer); + this.observers_[which].erase(callback); +} + + +/** + * Helper class that knows how to observe preference changes and + * invoke a callback when they do + * + * @constructor + * @param callback Function to call when the preference changes + */ +function G_PreferenceObserver(callback) { + this.debugZone = "prefobserver"; + this.callback_ = callback; +} + +/** + * Invoked by the pref system when a preference changes. Passes the + * message along to the callback. + * + * @param subject The nsIPrefBranch that changed + * @param topic String "nsPref:changed" (aka + * NS_PREFBRANCH_PREFCHANGE_OBSERVER_ID -- but where does it + * live???) + * @param data Name of the pref that changed + */ +G_PreferenceObserver.prototype.observe = function(subject, topic, data) { + G_Debug(this, "Observed pref change: " + data); + this.callback_(data); +} + +/** + * XPCOM cruft + * + * @param iid Interface id of the interface the caller wants + */ +G_PreferenceObserver.prototype.QueryInterface = function(iid) { + var Ci = Ci; + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIObserves) || + iid.equals(Ci.nsISupportsWeakReference)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; +} + + +// UNITTESTS + +function TEST_G_Preferences() { + if (G_GDEBUG) { + var z = "preferences UNITTEST"; + G_debugService.enableZone(z); + G_Debug(z, "Starting"); + + var p = new G_Preferences(); + + var testPref = "test-preferences-unittest"; + var noSuchPref = "test-preferences-unittest-aypabtu"; + + // Used to test observing + var observeCount = 0; + function observe(prefChanged) { + G_Assert(z, prefChanged == testPref, "observer broken"); + observeCount++; + }; + + // Test setting, getting, and observing + p.addObserver(testPref, observe); + p.setBoolPref(testPref, true); + G_Assert(z, p.getBoolPref(testPref), "get or set broken"); + G_Assert(z, observeCount == 1, "observer adding not working"); + + p.removeObserver(testPref, observe); + + p.setBoolPref(testPref, false); + G_Assert(z, observeCount == 1, "observer removal not working"); + G_Assert(z, !p.getBoolPref(testPref), "get broken"); + try { + p.getBoolPref(noSuchPref); + G_Assert(z, false, "getting non-existent pref didn't throw"); + } catch (e) { + } + + // Try the default varieties + G_Assert(z, + p.getBoolPrefOrDefault(noSuchPref, true), "default borken (t)"); + G_Assert(z, !p.getBoolPrefOrDefault(noSuchPref, false), "default borken"); + + // And the default-and-set variety + G_Assert(z, p.getBoolPrefOrDefaultAndSet(noSuchPref, true), + "default and set broken (didnt default"); + G_Assert(z, + p.getBoolPref(noSuchPref), "default and set broken (didnt set)"); + + // Remember to clean up the prefs we've set, and test removing prefs + // while we're at it + p.clearPref(noSuchPref); + G_Assert(z, !p.getBoolPrefOrDefault(noSuchPref, false), "clear broken"); + + p.clearPref(testPref); + + G_Debug(z, "PASSED"); + } +} diff --git a/mozilla/toolkit/components/protection/content/moz/protocol4.js b/mozilla/toolkit/components/protection/content/moz/protocol4.js new file mode 100644 index 00000000000..57d7e0e398a --- /dev/null +++ b/mozilla/toolkit/components/protection/content/moz/protocol4.js @@ -0,0 +1,164 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// A helper class that knows how to parse from and serialize to +// protocol4. This is a simple, historical format used by some Google +// interfaces, for example the Toolbar (i.e., ancient services). +// +// Protocol4 consists of a newline-separated sequence of name/value +// pairs (strings). Each line consists of the name, the value length, +// and the value itself, all separated by colons. Example: +// +// foo:6:barbaz\n +// fritz:33:issickofdynamicallytypedlanguages\n + + +/** + * This class knows how to serialize/deserialize maps to/from their + * protocol4 representation. + * + * @constructor + */ +function G_Protocol4Parser() { + this.debugZone = "protocol4"; + + this.protocol4RegExp_ = new RegExp("([^:]+):\\d+:(.*)$"); + this.newlineRegExp_ = new RegExp("(\\r)?\\n"); +} + +/** + * Create a map from a protocol4 string. Silently skips invalid lines. + * + * @param text String holding the protocol4 representation + * + * @returns Object as an associative array with keys and values + * given in text. The empty object is returned if none + * are parsed. + */ +G_Protocol4Parser.prototype.parse = function(text) { + + var response = {}; + if (!text) + return response; + + // Responses are protocol4: (repeated) name:numcontentbytes:content\n + var lines = text.split(this.newlineRegExp_); + for (var i = 0; i < lines.length; i++) + if (this.protocol4RegExp_.exec(lines[i])) + response[RegExp.$1] = RegExp.$2; + + return response; +} + +/** + * Create a protocol4 string from a map (object). Throws an error on + * an invalid input. + * + * @param map Object as an associative array with keys and values + * given as strings. + * + * @returns text String holding the protocol4 representation + */ +G_Protocol4Parser.prototype.serialize = function(map) { + if (typeof map != "object") + throw new Error("map must be an object"); + + var text = ""; + for (var key in map) { + if (typeof map[key] != "string") + throw new Error("Keys and values must be strings"); + + text += key + ":" + map[key].length + ":" + map[key] + "\n"; + } + + return text; +} + + +/** + * Cheesey unittests + */ +function TEST_G_Protocol4Parser() { + if (G_GDEBUG) { + var z = "protocol4 UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + var p = new G_Protocol4Parser(); + + function isEmpty(map) { + for (var key in map) + return false; + return true; + }; + + G_Assert(z, isEmpty(p.parse(null)), "Parsing null broken"); + G_Assert(z, isEmpty(p.parse("")), "Parsing nothing broken"); + + var t = "foo:3:bar"; + G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing one line broken"); + + t = "foo:3:bar\n"; + G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing line with lf broken"); + + t = "foo:3:bar\r\n"; + G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing with crlf broken"); + + + t = "foo:3:bar\nbar:3:baz\r\nbom:3:yaz\n"; + G_Assert(z, p.parse(t)["foo"] === "bar", "First in multiline"); + G_Assert(z, p.parse(t)["bar"] === "baz", "Second in multiline"); + G_Assert(z, p.parse(t)["bom"] === "yaz", "Third in multiline"); + G_Assert(z, p.parse(t)[""] === undefined, "Non-existent in multiline"); + + // Test serialization + + var original = { + "1": "1", + "2": "2", + "foobar": "baz", + "hello there": "how are you?" , + }; + var deserialized = p.parse(p.serialize(original)); + for (var key in original) + G_Assert(z, original[key] === deserialized[key], + "Trouble (de)serializing " + key); + + G_Debug(z, "PASSED"); + } +} diff --git a/mozilla/toolkit/components/protection/content/url-canonicalizer.js b/mozilla/toolkit/components/protection/content/url-canonicalizer.js new file mode 100644 index 00000000000..d7f54ca3b6c --- /dev/null +++ b/mozilla/toolkit/components/protection/content/url-canonicalizer.js @@ -0,0 +1,351 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// This is the class we use to canonicalize URLs for TRTables of type +// url. We maximally URL-decode the URL, treating +'s as if they're +// not special. We then specially URL-encode it (we encode ASCII +// values [0, 32] (whitespace or unprintable), 37 (%), [127, 255] +// (unprintable)). +// +// This mapping is not a function. That is, multiple URLs can map to +// the same canonical representation. However this is OK because +// collisions happen only when there are weird characters (e.g., +// nonprintables), and the canonical representation makes us robust +// to some weird kinds of encoding we could see. +// +// All members are static at this point -- this is basically a namespace. + + +/** + * Create a new URLCanonicalizer. Useless because members are static. + * + * @constructor + */ +function PROT_URLCanonicalizer() { + throw new Error("No need to instantiate a canonicalizer at this point."); +} + +PROT_URLCanonicalizer.debugZone = "urlcanonicalizer"; + +PROT_URLCanonicalizer.hexChars_ = "0123456789ABCDEF"; + +/** + * Helper funciton to (maybe) convert a two-character hex string into its + * decimal numerical equivalent + * + * @param hh String of length two that might be a valid hex sequence + * + * @returns Number: NaN if hh wasn't valid hex, else the appropriate decimal + * value + */ +PROT_URLCanonicalizer.hexPairToInt_ = function(hh) { + return Number("0x" + hh); +} + +/** + * Helper function to hex-encode a number + * + * @param val Number in range [0, 255] + * + * @returns String containing the hex representation of that number (sans 0x) + */ +PROT_URLCanonicalizer.toHex_ = function(val) { + var retVal = PROT_URLCanonicalizer.hexChars_.charAt((val >> 4) & 15) + + PROT_URLCanonicalizer.hexChars_.charAt(val & 15); + return retVal; +} + +/** + * Get the canonical version of the given URL for lookup in a table of + * type -url. + * + * @param url String to canonicalize + * + * @returns String containing the canonicalized url (maximally url-decoded, + * then specially url-encoded) + */ +PROT_URLCanonicalizer.canonicalizeURL_ = function(url) { + var arrayOfASCIIVals = PROT_URLCanonicalizer.fullyDecodeURL_(url); + return PROT_URLCanonicalizer.specialEncodeURL_(arrayOfASCIIVals); +} + +/** + * Maximally URL-decode a URL. This breaks the semantics of the URL, but + * we don't care because we're using it for lookup, not for navigation. + * We break multi-byte UTF-8 escape sequences as well, but we don't care + * so long as they canonicalize the same way consistently (they do). + * + * @param url String containing the URL to maximally decode. Should ONLY + * contain characters with UCS codepoints U+0001 to U+00FF + * (the ASCII set minus null). + * + * @returns Array of ASCII values corresponding to the decoded sequence of + * characters in the url + */ +PROT_URLCanonicalizer.fullyDecodeURL_ = function(url) { + + // The goals here are: simplicity, correctness, and most of all + // portability; we want the same implementation of canonicalization + // wherever we use it so as to to minimize the chances of + // inconsistency. For example, we have to do this canonicalization + // on URLs we get from third parties, and at the lookup server when + // we get a request. + // + // The following implementation should translate easily to any + // language that supports arrays and pointers or references. Note + // that arrays are pointer types in JavaScript, so foo = [some, + // array] points foo at the array; it doesn't copy it. The + // implementation is efficient (linear) so long as most %'s in the + // url belong to valid escape sequences and there aren't too many + // doubly-escaped values. + + // The basic idea is to copy current input to output, decoding escape + // sequences as we see them, until we decode a %. At that point we start + // copying into the "next iteration buffer" instead of the output buffer; + // we do this so we can accomodate multiply-escaped strings. When we hit + // the end of the input, we take the "next iteration buffer" as our input, + // and start over. + + var nextIteration = url.split(""); + var output = []; + + while (nextIteration.length) { + + var decodedAPercent = false; + var thisIteration = nextIteration; + var nextIteration = []; + + var i = 0; + while (i < thisIteration.length) { + + var c = thisIteration[i]; + if (c == "%" && i + 2 < thisIteration.length) { + + // Peek ahead to see if we have a valid HH sequence + var asciiVal = + PROT_URLCanonicalizer.hexPairToInt_(thisIteration[i + 1] + + thisIteration[i + 2]); + if (!isNaN(asciiVal)) { + i += 2; // Valid HH sequence; consume it + + if (asciiVal == 0) // We special case nulls + asciiVal = 1; + + c = String.fromCharCode(asciiVal); + if (c == "%") + decodedAPercent = true; + } + } + + if (decodedAPercent) + nextIteration[nextIteration.length] = c; + else + output[output.length] = c.charCodeAt(0); + + ++i; + } + } + + return output; +} + +/** + * Maximally URL-decode a URL (same as fullyDecodeURL_ except that it + * returns a string). Useful for making unittests more readable. + * + * @param url String containing the URL to maximally decode. Should ONLY + * contain characters with UCS codepoints U+0001 to U+00FF + * (the ASCII set minus null). + * + * @returns String containing the decoded URL + */ +PROT_URLCanonicalizer.fullyDecodeURLAsString_ = function(url) { + var arrayOfASCIIVals = PROT_URLCanonicalizer.fullyDecodeURL_(url); + var s = ""; + for (var i = 0; i < arrayOfASCIIVals.length; i++) + s += String.fromCharCode(arrayOfASCIIVals[i]); + return s; +} + +/** + * Specially URL-encode the given array of ASCII values. We want to encode + * the charcters: [0, 32], 37, [127, 255]. + * + * @param arrayOfASCIIValues Array of ascii values (numbers) to encode + * + * @returns String corresonding to the escaped URL + */ +PROT_URLCanonicalizer.specialEncodeURL_ = function(arrayOfASCIIValues) { + + var output = []; + for (var i = 0; i < arrayOfASCIIValues.length; i++) { + var n = arrayOfASCIIValues[i]; + + if (n <= 32 || n == 37 || n >= 127) + output.push("%" + ((!n) ? "01" : PROT_URLCanonicalizer.toHex_(n))); + else + output.push(String.fromCharCode(n)); + } + + return output.join(""); +} + + +/** + * Lame unittesting function + */ +function TEST_PROT_URLCanonicalizer() { + if (G_GDEBUG) { + var z = "urlcanonicalizer UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + + // ------ TEST HEX GOTCHA ------ + + var hexify = PROT_URLCanonicalizer.toHex_; + var shouldHaveLeadingZeros = hexify(0) + hexify(1); + G_Assert(z, shouldHaveLeadingZeros == "0001", + "Need to append leading zeros to hex rep value <= 15 !") + + + // ------ TEST DECODING ------ + + // For convenience, shorten the function name + var dec = PROT_URLCanonicalizer.fullyDecodeURLAsString_; + + // Test empty string + G_Assert(z, dec("") == "", "decoding empty string"); + + // Test decoding of all characters + var allCharsEncoded = ""; + var allCharsEncodedLowercase = ""; + var allCharsAsString = ""; + // Special case null + allCharsEncoded += "%01"; + allCharsEncodedLowercase += "%01"; + allCharsAsString += String.fromCharCode(1); + for (var i = 1; i < 256; i++) { + allCharsEncoded += "%" + PROT_URLCanonicalizer.toHex_(i); + allCharsEncodedLowercase += "%" + + PROT_URLCanonicalizer.toHex_(i).toLowerCase(); + allCharsAsString += String.fromCharCode(i); + } + G_Assert(z, dec(allCharsEncoded) == allCharsAsString, "decoding escaped"); + G_Assert(z, dec(allCharsEncodedLowercase) == allCharsAsString, + "decoding lowercase"); + + // Test %-related edge cases + G_Assert(z, dec("%") == "%", "1 percent"); + G_Assert(z, dec("%xx") == "%xx", "1 percent, two non-hex"); + G_Assert(z, dec("%%") == "%%", "2 percent"); + G_Assert(z, dec("%%%") == "%%%", "3 percent"); + G_Assert(z, dec("%%%%") == "%%%%", "4 percent"); + G_Assert(z, dec("%1") == "%1", "1 percent, one nonhex"); + G_Assert(z, dec("%1z") == "%1z", "1 percent, two nonhex"); + G_Assert(z, dec("a%1z") == "a%1z", "nonhex, 1 percent, two nonhex"); + G_Assert(z, dec("abc%d%e%fg%hij%klmno%") == "abc%d%e%fg%hij%klmno%", + "lots of percents, no hex"); + + // Test repeated %-decoding. Note: %25 --> %, %32 --> 2, %35 --> 5 + G_Assert(z, dec("%25") == "%", "single-encoded %"); + G_Assert(z, dec("%25%32%35") == "%", "double-encoded %"); + G_Assert(z, dec("asdf%25%32%35asd") == "asdf%asd", "double-encoded % 2"); + G_Assert(z, dec("%%%25%32%35asd%%") == "%%%asd%%", "double-encoded % 3"); + G_Assert(z, dec("%25%32%35%25%32%35%25%32%35") == "%%%", + "sequenctial double-encoded %"); + G_Assert(z, dec("%2525252525252525") == "%", "many-encoded %"); + G_Assert(z, dec("%257Ea%2521b%2540c%2523d%2524e%25f%255E00%252611%252A22%252833%252944_55%252B") == "~a!b@c#d$e%f^00&11*22(33)44_55+", + "4x-encoded string"); + + + // ------ TEST ENCODING ------ + + // For convenience, shorten the function name + var enc = PROT_URLCanonicalizer.specialEncodeURL_; + + // Test empty string + G_Assert(z, enc([]) == "", "encoding empty array"); + + // Test that all characters we shouldn't encode ([33-36],[38,126]) are not. + var no = []; + var noAsString = ""; + for (var i = 33; i < 127; i++) + if (i != 37) { // skip % + no.push(i); + noAsString += String.fromCharCode(i); + } + G_Assert(z, enc(no) == noAsString, "chars to not encode"); + + // Test that all the chars that we should encode [0,32],37,[127,255] are + var yes = []; + var yesAsString = ""; + var yesExpectedString = ""; + // Special case 0 + yes.push(0); + yesAsString += String.fromCharCode(1); + yesExpectedString += "%01"; + for (var i = 1; i < 256; i++) + if (i < 33 || i == 37 || i > 126) { + yes.push(i); + yesAsString += String.fromCharCode(i); + var hex = i.toString(16).toUpperCase(); + yesExpectedString += "%" + ((i < 16) ? "0" : "") + hex; + } + G_Assert(z, enc(yes) == yesExpectedString, "chars to encode"); + // Can't use decodeURIComponent or encodeURIComponent to test b/c UTF-8 + + + // ------ TEST COMPOSITION ------ + + // For convenience, shorten function name: + var c = PROT_URLCanonicalizer.canonicalizeURL_; + + G_Assert(z, c("http://www.google.com") == "http://www.google.com", + "http://www.google.com"); + G_Assert(z, c("http://%31%36%38%2e%31%38%38%2e%39%39%2e%32%36/%2E%73%65%63%75%72%65/%77%77%77%2E%65%62%61%79%2E%63%6F%6D/") == "http://168.188.99.26/.secure/www.ebay.com/", + "fully encoded ebay"); + G_Assert(z, c("http://195.127.0.11/uploads/%20%20%20%20/.verify/.eBaysecure=updateuserdataxplimnbqmn-xplmvalidateinfoswqpcmlx=hgplmcx/") == "http://195.127.0.11/uploads/%20%20%20%20/.verify/.eBaysecure=updateuserdataxplimnbqmn-xplmvalidateinfoswqpcmlx=hgplmcx/", + "long url with spaces that stays same"); + + // TODO: MORE! + + G_Debug(z, "PASSED"); + } +} + diff --git a/mozilla/toolkit/components/protection/content/url-crypto-key-manager.js b/mozilla/toolkit/components/protection/content/url-crypto-key-manager.js new file mode 100644 index 00000000000..459d4b41b01 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/url-crypto-key-manager.js @@ -0,0 +1,391 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// This file implements the tricky business of managing the keys for our +// URL encryption. The protocol is: +// +// - Server generates secret key K_S +// - Client starts up and requests a new key K_C from the server via HTTPS +// - Server generates K_C and WrappedKey, which is K_C encrypted with K_S +// - Server resonse with K_C and WrappedKey +// - When client wants to encrypt a URL, it encrypts it with K_C and sends +// the encrypted URL along with WrappedKey +// - Server decrypts WrappedKey with K_S to get K_C, and the URL with K_C +// +// This is, however, trickier than it sounds for two reasons. First, +// we want to keep the number of HTTPS requests to an aboslute minimum +// (like 1 or 2 per browser session). Second, the HTTPS request at +// startup might fail, for example the user might be offline or a URL +// fetch might need to be issued before the HTTPS request has +// completed. +// +// We implement the following policy: +// +// - The extension will issue at most two HTTPS getkey requests per session +// - The extension will issue one HTTPS getkey request at startup +// - The extension will serialize to disk any key it gets +// - The extension will fall back on this serialized key until it has a +// fresh key +// - The front-end can respond with a flag in a lookup request that tells +// the client to re-key. The client will issue a new HTTPS getkey request +// at this time if it has only issued one before + + +/** + * A key manager for UrlCrypto. There should be exactly one of these + * per appplication, and all UrlCrypto's should share it. This is + * currently implemented by having the manager attach itself to the + * UrlCrypto's prototype at startup. We could've opted for a global + * instead, but I like this better, even though it is spooky action + * at a distance. + * + * @param opt_keyFilename String containing the name of the + * file we should serialize keys to/from. Used + * mostly for testing. + * + * @param opt_testing Boolean indicating whether we are testing. If we + * are, then we skip trying to read the old key from + * file and automatically trying to rekey; presumably + * the tester will drive these manually. + * + * @constructor + */ +function PROT_UrlCryptoKeyManager(opt_keyFilename, opt_testing) { + this.debugZone = "urlcryptokeymanager"; + this.testing_ = !!opt_testing; + this.base64_ = new G_Base64(); + this.clientKey_ = null; // Base64-encoded, as fetched from server + this.clientKeyArray_ = null; // Base64-decoded into an array of numbers + this.wrappedKey_ = null; // Opaque websafe base64-encoded server key + this.rekeyTries_ = 0; + + this.keyFilename_ = opt_keyFilename ? + opt_keyFilename : + PROT_GlobalStore.getKeyFilename(); + + // Convenience properties + this.MAX_REKEY_TRIES = PROT_UrlCryptoKeyManager.MAX_REKEY_TRIES; + this.CLIENT_KEY_NAME = PROT_UrlCryptoKeyManager.CLIENT_KEY_NAME; + this.WRAPPED_KEY_NAME = PROT_UrlCryptoKeyManager.WRAPPED_KEY_NAME; + + if (!this.testing_) { + G_Assert(this, !PROT_UrlCrypto.prototype.manager_, + "Already have manager?"); + PROT_UrlCrypto.prototype.manager_ = this; + + this.maybeLoadOldKey(); + this.reKey(); + } +} + +// Do ***** NOT ***** set this higher; HTTPS is expensive +PROT_UrlCryptoKeyManager.MAX_REKEY_TRIES = 2; + +// These are the names the server will respond with in protocol4 format +PROT_UrlCryptoKeyManager.CLIENT_KEY_NAME = "clientkey"; +PROT_UrlCryptoKeyManager.WRAPPED_KEY_NAME = "wrappedkey"; + + +/** + * Called by a UrlCrypto to get the current K_C + * + * @returns Array of numbers making up the client key or null if we + * have no key + */ +PROT_UrlCryptoKeyManager.prototype.getClientKeyArray = function() { + return this.clientKeyArray_; +} + +/** + * Called by a UrlCrypto to get WrappedKey + * + * @returns Opaque base64-encoded WrappedKey or null if we haven't + * gotten one + */ +PROT_UrlCryptoKeyManager.prototype.getWrappedKey = function() { + return this.wrappedKey_; +} + +/** + * Tell the manager to re-key. For safety, this method still obeys the + * max-tries limit. Clients should generally use maybeReKey() if they + * want to try a re-keying: it's an error to call reKey() after we've + * hit max-tries, but not an error to call maybeReKey(). + */ +PROT_UrlCryptoKeyManager.prototype.reKey = function() { + + if (this.rekeyTries_ > this.MAX_REKEY_TRIES) + throw new Error("Have already rekeyed " + this.rekeyTries_ + " times"); + + this.rekeyTries_++; + + G_Debug(this, "Attempting to re-key"); + var url = PROT_GlobalStore.getGetKeyURL(); + if (!this.testing_) + (new PROT_XMLFetcher()).get(url, + BindToObject(this.onGetKeyResponse, this)); +} + +/** + * Try to re-key if we haven't already hit our limit. It's OK to call + * this method multiple times, even if we've already tried to rekey + * more than the max. It will simply refuse to do so. + * + * @returns Boolean indicating if it actually issued a rekey request (that + * is, if we haven' already hit the max) + */ +PROT_UrlCryptoKeyManager.prototype.maybeReKey = function() { + if (this.rekeyTries_ > this.MAX_REKEY_TRIES) { + G_Debug(this, "Not re-keying; already at max"); + return false; + } + + this.reKey(); + return true; +} + +/** + * @returns Boolean indicating if we have a key we can use + */ +PROT_UrlCryptoKeyManager.prototype.hasKey_ = function() { + return this.clientKey_ != null && this.wrappedKey_ != null; +} + +/** + * Set a new key and serialize it to disk. + * + * @param clientKey String containing the base64-encoded client key + * we wish to use + * + * @param wrappedKey String containing the opaque base64-encoded WrappedKey + * the server gave us (i.e., K_C encrypted with K_S) + */ +PROT_UrlCryptoKeyManager.prototype.replaceKey_ = function(clientKey, + wrappedKey) { + if (this.clientKey_) + G_Debug(this, "Replacing " + this.clientKey_ + " with " + clientKey); + + this.clientKey_ = clientKey; + this.clientKeyArray_ = this.base64_.decodeString(this.clientKey_); + this.wrappedKey_ = wrappedKey; + + this.serializeKey_(this.clientKey_, this.wrappedKey_); +} + +/** + * Try to write the key to disk so we can fall back on it. Fail + * silently if we cannot. The keys are serialized in protocol4 format. + * + * @returns Boolean indicating whether we succeeded in serializing + */ +PROT_UrlCryptoKeyManager.prototype.serializeKey_ = function() { + + var map = {}; + map[this.CLIENT_KEY_NAME] = this.clientKey_; + map[this.WRAPPED_KEY_NAME] = this.wrappedKey_; + + try { + + var appDir = new PROT_ApplicationDirectory(); + if (!appDir.exists()) + appDir.create(); + var keyfile = appDir.getAppDirFileInterface(); + keyfile.append(this.keyFilename_); + G_FileWriter.writeAll(keyfile, (new G_Protocol4Parser).serialize(map)); + return true; + + } catch(e) { + + G_Error(this, "Failed to serialize new key: " + e); + return false; + + } +} + +/** + * Invoked when we've received a protocol4 response to our getkey + * request. Try to parse it and set this key as the new one if we can. + * + * @param responseText String containing the protocol4 getkey response + */ +PROT_UrlCryptoKeyManager.prototype.onGetKeyResponse = function(responseText) { + + var response = (new G_Protocol4Parser).parse(responseText); + var clientKey = response[this.CLIENT_KEY_NAME]; + var wrappedKey = response[this.WRAPPED_KEY_NAME]; + + if (response && clientKey && wrappedKey) { + G_Debug(this, "Got new key from: " + responseText); + this.replaceKey_(clientKey, wrappedKey); + } else { + G_Debug(this, "Not a valid response for /getkey"); + } +} + +/** + * Attempt to read a key we've previously serialized from disk, so + * that we can fall back on it in case we can't get one from the + * server. If we get a key, only use it if we don't already have one + * (i.e., if our startup HTTPS request died or hasn't yet completed). + * + * This method should be invoked early, like when the user's profile + * becomes available. + */ +PROT_UrlCryptoKeyManager.prototype.maybeLoadOldKey = function() { + + var oldKey = null; + try { + var appDir = new PROT_ApplicationDirectory(); + var keyfile = appDir.getAppDirFileInterface(); + keyfile.append(this.keyFilename_); + if (keyfile.exists()) + oldKey = G_FileReader.readAll(keyfile); + } catch(e) { + G_Debug(this, "Caught " + e + " trying to read keyfile"); + return; + } + + if (!oldKey) { + G_Debug(this, "Couldn't find old key."); + return; + } + + oldKey = (new G_Protocol4Parser).parse(oldKey); + var clientKey = oldKey[this.CLIENT_KEY_NAME]; + var wrappedKey = oldKey[this.WRAPPED_KEY_NAME]; + + if (oldKey && clientKey && wrappedKey && !this.hasKey_()) { + G_Debug(this, "Read old key from disk."); + this.replaceKey_(clientKey, wrappedKey); + } +} + + + +/** + * Cheesey tests + */ +function TEST_PROT_UrlCryptoKeyManager() { + if (G_GDEBUG) { + var z = "urlcryptokeymanager UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + // Let's not clobber any real keyfile out there + var kf = "keytest.txt"; + + // Let's be able to clean up after ourselves + function removeTestFile(f) { + var appDir = new PROT_ApplicationDirectory(); + var file = appDir.getAppDirFileInterface(); + file.append(f); + if (file.exists()) + file.remove(false /* do not recurse */); + }; + removeTestFile(kf); + + var km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); + + // CASE: simulate nothing on disk, then get something from server + + G_Assert(z, !km.hasKey_(), "KM already has key?"); + km.maybeLoadOldKey(); + G_Assert(z, !km.hasKey_(), "KM loaded non-existent key?"); + km.onGetKeyResponse(null); + G_Assert(z, !km.hasKey_(), "KM got key from null response?"); + km.onGetKeyResponse(""); + G_Assert(z, !km.hasKey_(), "KM got key from empty response?"); + km.onGetKeyResponse("aslkaslkdf:34:a230\nskdjfaljsie"); + G_Assert(z, !km.hasKey_(), "KM got key from garbage response?"); + + var realResponse = "clientkey:24:zGbaDbx1pxoYe7siZYi8VA==\n" + + "wrappedkey:24:MTr1oDt6TSOFQDTvKCWz9PEn"; + km.onGetKeyResponse(realResponse); + // Will have written it to file as a side effect + G_Assert(z, km.hasKey_(), "KM couldn't get key from real response?"); + G_Assert(z, km.clientKey_ == "zGbaDbx1pxoYe7siZYi8VA==", + "Parsed wrong client key from response?"); + G_Assert(z, km.wrappedKey_ == "MTr1oDt6TSOFQDTvKCWz9PEn", + "Parsed wrong wrapped key from response?"); + + // CASE: simulate something on disk, then get something from server + + km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); + G_Assert(z, !km.hasKey_(), "KM already has key?"); + km.maybeLoadOldKey(); + G_Assert(z, km.hasKey_(), "KM couldn't load existing key from disk?"); + G_Assert(z, km.clientKey_ == "zGbaDbx1pxoYe7siZYi8VA==", + "Parsed wrong client key from disk?"); + G_Assert(z, km.wrappedKey_ == "MTr1oDt6TSOFQDTvKCWz9PEn", + "Parsed wrong wrapped key from disk?"); + var realResponse2 = "clientkey:24:dtmbEN1kgN/LmuEoYifaFw==\n" + + "wrappedkey:24:MTpPH3pnLDKihecOci+0W5dk"; + km.onGetKeyResponse(realResponse2); + // Will have written it to disk + G_Assert(z, km.hasKey_(), "KM couldn't replace key from server response?"); + G_Assert(z, km.clientKey_ == "dtmbEN1kgN/LmuEoYifaFw==", + "Replace client key from server failed?"); + G_Assert(z, km.wrappedKey_ == "MTpPH3pnLDKihecOci+0W5dk", + "Replace wrapped key from server failed?"); + + // CASE: check overwriting a key on disk + + km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); + G_Assert(z, !km.hasKey_(), "KM already has key?"); + km.maybeLoadOldKey(); + G_Assert(z, km.hasKey_(), "KM couldn't load existing key from disk?"); + G_Assert(z, km.clientKey_ == "dtmbEN1kgN/LmuEoYifaFw==", + "Replace client on from disk failed?"); + G_Assert(z, km.wrappedKey_ == "MTpPH3pnLDKihecOci+0W5dk", + "Replace wrapped key on disk failed?"); + + // Test that we only fetch at most two getkey's per lifetime of the manager + + km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); + km.reKey(); + for (var i = 0; i < km.MAX_REKEY_TRIES; i++) + G_Assert(z, km.maybeReKey(), "Couldn't rekey?"); + G_Assert(z, !km.maybeReKey(), "Rekeyed when max hit"); + + removeTestFile(kf); + + G_Debug(z, "PASSED"); + + } +} + diff --git a/mozilla/toolkit/components/protection/content/url-crypto.js b/mozilla/toolkit/components/protection/content/url-crypto.js new file mode 100644 index 00000000000..0ac9bcbc882 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/url-crypto.js @@ -0,0 +1,689 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * Monica Chew + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// This file implements our query param encryption. You hand it a set +// of query params, and it will hand you a set of (maybe) encrypted +// query params back. It takes the query params you give it, +// encodes and encrypts them into a encrypted query param, and adds +// the extra query params the server will need to decrypt them +// (e.g., the version of encryption and the decryption key). +// +// The key manager provides the keys we need; this class just focuses +// on encrypting query params. See the url crypto key manager for +// details of our protocol, but essentially encryption is +// RC4_key(input) with key == MD5(K_C || nonce) where nonce is a +// 32-bit integer appended big-endian and K_C is the client's key. +// +// If for some reason we don't have an encryption key, encrypting is the +// identity function. + +/** + * This class knows how to encrypt query parameters that will be + * understood by the lookupserver. + * + * @constructor + */ +function PROT_UrlCrypto() { + this.debugZone = "urlcrypto"; + this.hasher_ = new G_CryptoHasher(); + this.base64_ = new G_Base64(); + this.rc4_ = new ARC4(); + + if (!this.manager_) { + // Create a UrlCryptoKeyManager to reads keys from profile directory if + // one doesn't already exist. UrlCryptoKeyManager puts a reference to + // itself on PROT_UrlCrypto.prototype (this also prevents garbage + // collection). + new PROT_UrlCryptoKeyManager(); + } + + // Convenience properties + this.VERSION = PROT_UrlCrypto.VERSION; + this.RC4_DISCARD_BYTES = PROT_UrlCrypto.RC4_DISCARD_BYTES; + this.VERSION_QUERY_PARAM_NAME = PROT_UrlCrypto.QPS.VERSION_QUERY_PARAM_NAME; + this.ENCRYPTED_PARAMS_PARAM_NAME = + PROT_UrlCrypto.QPS.ENCRYPTED_PARAMS_PARAM_NAME; + this.COUNT_QUERY_PARAM_NAME = PROT_UrlCrypto.QPS.COUNT_QUERY_PARAM_NAME; + this.WRAPPEDKEY_QUERY_PARAM_NAME = + PROT_UrlCrypto.QPS.WRAPPEDKEY_QUERY_PARAM_NAME; + + // Properties for computing macs + this.macer_ = new G_CryptoHasher(); // don't use hasher_ + this.macInitialized_ = false; + // Separator to prevent leakage between key and data when computing mac + this.separator_ = ":coolgoog:"; + this.separatorArray_ = this.base64_.arrayifyString(this.separator_); +} + +// The version of encryption we implement +PROT_UrlCrypto.VERSION = "1"; + +PROT_UrlCrypto.RC4_DISCARD_BYTES = 1600; + +// The query params are we going to send to let the server know what is +// encrypted, and how +PROT_UrlCrypto.QPS = {}; +PROT_UrlCrypto.QPS.VERSION_QUERY_PARAM_NAME = "encver"; +PROT_UrlCrypto.QPS.ENCRYPTED_PARAMS_PARAM_NAME = "encparams"; +PROT_UrlCrypto.QPS.COUNT_QUERY_PARAM_NAME = "nonce"; +PROT_UrlCrypto.QPS.WRAPPEDKEY_QUERY_PARAM_NAME = "wrkey"; + +/** + * @returns Reference to the keymanager (if one exists), else undefined + */ +PROT_UrlCrypto.prototype.getManager = function() { + return this.manager_; +} + +/** + * Helper method that takes a map of query params (param name -> + * value) and turns them into a query string. Note that it encodes + * the values as it writes the string. + * + * @param params Object (map) of query names to values. Values should + * not be uriencoded. + * + * @returns String of query params from the map. Values will be uri + * encoded + */ +PROT_UrlCrypto.prototype.appendParams_ = function(params) { + var queryString = ""; + for (var param in params) + queryString += "&" + param + "=" + encodeURIComponent(params[param]); + + return queryString; +} + +/** + * Encrypt a set of query params if we can. If we can, we return a new + * set of query params that should be added to a query string. The set + * of query params WILL BE different than the input query params if we + * can encrypt (e.g., there will be extra query params with meta- + * information such as the version of encryption we're using). If we + * can't encrypt, we just return the query params we're passed. + * + * @param params Object (map) of query param names to values. Values should + * not be uriencoded. + * + * @returns Object (map) of query param names to values. Values are NOT + * uriencoded; the caller should encode them as it writes them + * to a proper query string. + */ +PROT_UrlCrypto.prototype.maybeCryptParams = function(params) { + if (!this.manager_) + throw new Error("Need a key manager for UrlCrypto"); + if (typeof params != "object") + throw new Error("params is an associative array of name/value params"); + + var clientKeyArray = this.manager_.getClientKeyArray(); + var wrappedKey = this.manager_.getWrappedKey(); + + // No keys? Can't encrypt. Damn. + if (!clientKeyArray || !wrappedKey) { + G_Debug(this, "No key; can't encrypt query params"); + return params; + } + + // Serialize query params to a query string that we will then + // encrypt and place in a special query param the front-end knows is + // encrypted. + var queryString = this.appendParams_(params); + + // Nonce, really. We want 32 bits; make it so. + var counter = this.getCount_(); + counter = counter & 0xFFFFFFFF; + + var encrypted = this.encryptV1(clientKeyArray, + this.VERSION, + counter, + this.base64_.arrayifyString(queryString)); + + params = {}; + params[this.VERSION_QUERY_PARAM_NAME] = this.VERSION; + params[this.COUNT_QUERY_PARAM_NAME] = counter; + params[this.WRAPPEDKEY_QUERY_PARAM_NAME] = wrappedKey; + params[this.ENCRYPTED_PARAMS_PARAM_NAME] = encrypted; + + return params; +} + +/** + * Encrypt something IN PLACE. Did you hear that? It works IN PLACE. + * That is, it replaces the plaintext with ciphertext. It also returns + * the websafe base64-encoded ciphertext. The client key is untouched. + * + * This method runs in about ~5ms on a 2Ghz P4. (Turn debugging off if + * you see it much slower). + * + * @param clientKeyArray Array of bytes (numbers in [0,255]) composing K_C + * + * @param version String indicating the version of encryption we should use. + * + * @param counter Number that acts as a nonce for this encryption + * + * @param inOutArray Array of plaintext bytes that will be replaced + * with the array of ciphertext bytes + * + * @returns String containing the websafe base64-encoded ciphertext + */ +PROT_UrlCrypto.prototype.encryptV1 = function(clientKeyArray, + version, + counter, + inOutArray) { + + // We're a version1 encrypter, after all + if (version != "1") + throw new Error("Unknown encryption version"); + + var key = this.deriveEncryptionKey(clientKeyArray, counter); + + this.rc4_.setKey(key, key.length); + + if (this.RC4_DISCARD_BYTES > 0) + this.rc4_.discard(this.RC4_DISCARD_BYTES); + + // The crypt() method works in-place + this.rc4_.crypt(inOutArray, inOutArray.length); + + return this.base64_.encodeByteArray(inOutArray, true /* websafe */); +} + +/** + * Create an encryption key from K_C and a nonce + * + * @param clientKeyArray Array of bytes comprising K_C + * + * @param count Number that acts as a nonce for this key + * + * @returns Array of bytes containing the encryption key + */ +PROT_UrlCrypto.prototype.deriveEncryptionKey = function(clientKeyArray, + count) { + G_Assert(this, clientKeyArray instanceof Array, + "Client key should be an array of bytes"); + G_Assert(this, typeof count == "number", "Count should be a number"); + + // Don't clobber the client key by appending the nonce; use another array + var paddingArray = []; + paddingArray.push(count >> 24); + paddingArray.push((count >> 16) & 0xFF); + paddingArray.push((count >> 8) & 0xFF); + paddingArray.push(count & 0xFF); + + this.hasher_.init(G_CryptoHasher.algorithms.MD5); + this.hasher_.updateFromArray(clientKeyArray); + this.hasher_.updateFromArray(paddingArray); + + return this.base64_.arrayifyString(this.hasher_.digestRaw()); +} + +/** + * Return a new nonce for us to use. Rather than keeping a counter and + * the headaches that entails, just use the low ms since the epoch. + * + * @returns 32-bit number that is the nonce to use for this encryption + */ +PROT_UrlCrypto.prototype.getCount_ = function() { + return ((new Date).getTime() & 0xFFFFFFFF); +} + +/** + * Init the mac. This function is called by WireFormatReader if the update + * server has sent along a mac param. The caller must not call initMac again + * before calling finishMac; instead, the caller should just use another + * UrlCrypto object. + * + * @param opt_clientKeyArray Optional clientKeyArray, for testing + */ +PROT_UrlCrypto.prototype.initMac = function(opt_clientKeyArray) { + if (this.macInitialized_) { + throw new Error("Can't interleave calls to initMac. Please use another " + + "UrlCrypto object."); + } + + this.macInitialized_ = true; + + var clientKeyArray = null; + + if (!!opt_clientKeyArray) { + clientKeyArray = opt_clientKeyArray; + } else { + clientKeyArray = this.manager_.getClientKeyArray(); + } + + // Don't re-use this.hasher_, in case someone calls deriveEncryptionKey + // between initMac and finishMac + this.macer_.init(G_CryptoHasher.algorithms.MD5); + + this.macer_.updateFromArray(clientKeyArray); + this.macer_.updateFromArray(this.separatorArray_); +} + +/** + * Add a line to the mac. Called by WireFormatReader.processLine. Not thread + * safe. + * + * @param s The string to add + */ +PROT_UrlCrypto.prototype.updateMacFromString = function(s) { + if (!this.macInitialized_) { + throw new Error ("Initialize mac first"); + } + + var arr = this.base64_.arrayifyString(s); + this.macer_.updateFromArray(arr); +} + +/** + * Finish up computing the mac. Not thread safe. + * + * @param opt_clientKeyArray Optional clientKeyArray, for testing + */ +PROT_UrlCrypto.prototype.finishMac = function(opt_clientKeyArray) { + var clientKeyArray = null; + if (!!opt_clientKeyArray) { + clientKeyArray = opt_clientKeyArray; + } else { + clientKeyArray = this.manager_.getClientKeyArray(); + } + + if (!this.macInitialized_) { + throw new Error ("Initialize mac first"); + } + this.macer_.updateFromArray(this.separatorArray_); + this.macer_.updateFromArray(clientKeyArray); + + this.macInitialized_ = false; + + return this.macer_.digestBase64(); +} + +/** + * Compute a mac over the whole data string, and return the base64-encoded + * string + * + * @param data A string + * @param opt_outputRaw True for raw output, false for base64 + * @param opt_clientKeyArray An optional key to pass in for testing + * @param opt_separatorArray An optional separator array to pass in for testing + * @returns MD5(key+separator+data+separator+key) + */ +PROT_UrlCrypto.prototype.computeMac = function(data, + opt_outputRaw, + opt_clientKeyArray, + opt_separatorArray) { + var clientKeyArray = null; + var separatorArray = null; + + // Get keys and such for testing + if (!!opt_clientKeyArray) { + clientKeyArray = opt_clientKeyArray; + } else { + clientKeyArray = this.manager_.getClientKeyArray(); + } + + if (!!opt_separatorArray) { + separatorArray = opt_separatorArray; + } else { + separatorArray = this.separatorArray_; + } + + this.macer_.init(G_CryptoHasher.algorithms.MD5); + + this.macer_.updateFromArray(clientKeyArray); + this.macer_.updateFromArray(separatorArray); + + // Note to self: calling G_CryptoHasher.updateFromString ain't the same as + // arrayifying the string and then calling updateFromArray. Not sure if + // that's a bug in G_CryptoHasher or not. Niels, what do you think? + var arr = this.base64_.arrayifyString(data); + this.macer_.updateFromArray(arr); + + this.macer_.updateFromArray(separatorArray); + this.macer_.updateFromArray(clientKeyArray); + + if (!!opt_outputRaw) { + return this.macer_.digestRaw(); + } + return this.macer_.digestBase64(); +} + +/** + * Cheeseball unittest + */ +function TEST_PROT_UrlCrypto() { + if (G_GDEBUG) { + var z = "urlcrypto UNITTEST"; + G_debugService.enableZone(z); + + G_Debug(z, "Starting"); + + // We set keys on the keymanager to ensure data flows properly, so + // make sure we can clean up after it + + var kf = "test.txt"; + function removeTestFile(f) { + var appDir = new PROT_ApplicationDirectory(); + var file = appDir.getAppDirFileInterface(); + file.append(f); + if (file.exists()) + file.remove(false /* do not recurse */ ); + }; + removeTestFile(kf); + + var km = new PROT_UrlCryptoKeyManager(kf, true /* testing */); + + // Test operation when it's not intialized + + var c = new PROT_UrlCrypto(); + + var fakeManager = { + getClientKeyArray: function() { return null; }, + getWrappedKey: function() { return null; }, + }; + c.manager_ = fakeManager; + + var params = { + foo: "bar", + baz: "bomb", + }; + G_Assert(z, c.maybeCryptParams(params)["foo"] === "bar", + "How can we encrypt if we don't have a key?"); + c.manager_ = km; + G_Assert(z, c.maybeCryptParams(params)["foo"] === "bar", + "Again, how can we encrypt if we don't have a key?"); + + // Now we have a key! See if we can get a crypted url + var realResponse = "clientkey:24:dtmbEN1kgN/LmuEoYifaFw==\n" + + "wrappedkey:24:MTpPH3pnLDKihecOci+0W5dk"; + km.onGetKeyResponse(realResponse); + var crypted = c.maybeCryptParams(params); + G_Assert(z, crypted["foo"] === undefined, "We have a key but can't crypt"); + G_Assert(z, crypted["bomb"] === undefined, "We have a key but can't crypt"); + + // Now check to make sure all the expected query params are there + for (var p in PROT_UrlCrypto.QPS) + G_Assert(z, crypted[PROT_UrlCrypto.QPS[p]] != undefined, + "Output query params doesn't have: " + PROT_UrlCrypto.QPS[p]); + + // Now test that encryption is determinisitic + var b64 = new G_Base64(); + + // Some helper functions + function arrayEquals(a1, a2) { + if (a1.length != a2.length) + return false; + + for (var i = 0; i < a1.length; i++) + if (typeof a1[i] != typeof a2[i] || a1[i] != a2[i]) + return false; + return true; + }; + + function arrayAsString(a) { + var s = "["; + for (var i = 0; i < a.length; i++) + s += a[i] + ","; + return s + "]"; + }; + + function printArray(a) { + var s = arrayAsString(a); + G_Debug(z, s); + }; + + var keySizeBytes = km.clientKeyArray_.length; + + var startCrypt = (new Date).getTime(); + var numCrypts = 0; + + // Set this to true for extended testing + var doLongTest = false; + if (doLongTest) { + + // Now run it through its paces. For a variety of keys of a + // variety of lengths, and a variety of coutns, encrypt + // plaintexts of different lengths + + // For a variety of key lengths... + for (var i = 0; i < 2 * keySizeBytes; i++) { + var clientKeyArray = []; + + // For a variety of keys... + for (var j = 0; j < keySizeBytes; j++) + clientKeyArray[j] = i + j; + + // For a variety of counts... + for (var count = 0; count < 40; count++) { + + var payload = ""; + + // For a variety of plaintexts of different lengths + for (var payloadPadding = 0; payloadPadding < count; payloadPadding++) + payload += "a"; + + var plaintext1 = b64.arrayifyString(payload); + var plaintext2 = b64.arrayifyString(payload); + var plaintext3 = b64.arrayifyString(payload); + + // Verify that encryption is deterministic given set parameters + numCrypts++; + var ciphertext1 = c.encryptV1(clientKeyArray, + "1", + count, + plaintext1); + + numCrypts++; + var ciphertext2 = c.encryptV1(clientKeyArray, + "1", + count, + plaintext2); + + G_Assert(z, ciphertext1 === ciphertext2, + "Two plaintexts having different ciphertexts:" + + ciphertext1 + " " + ciphertext2); + + numCrypts++; + + // Now verify that it is symmetrical + var ciphertext3 = c.encryptV1(clientKeyArray, + "1", + count, + b64.decodeString(ciphertext2), + true /* websafe */); + + G_Assert(z, arrayEquals(plaintext3, b64.decodeString(ciphertext3, + true/*websafe*/)), + "Encryption and decryption not symmetrical"); + } + } + + // Output some interesting info + var endCrypt = (new Date).getTime(); + var totalMS = endCrypt - startCrypt; + G_Debug(z, "Info: Did " + numCrypts + " encryptions in " + + totalMS + "ms, for an average of " + + (totalMS / numCrypts) + "ms per crypt()"); + } + + // Now check for compatability with C++ + + var ciphertexts = {}; + // Generated below, and tested in C++ as well. Ciphertexts is a map + // from substring lengths to encrypted values. + + ciphertexts[0]=""; + ciphertexts[1]="dA=="; + ciphertexts[2]="akY="; + ciphertexts[3]="u5mV"; + ciphertexts[4]="bhtioQ=="; + ciphertexts[5]="m2wSZnQ="; + ciphertexts[6]="zd6gWyDO"; + ciphertexts[7]="bBN0WVrlCg=="; + ciphertexts[8]="Z6U_6bMelFM="; + ciphertexts[9]="UVoiytL-gHzp"; + ciphertexts[10]="3Xr_ZMmdmvg7zw=="; + ciphertexts[11]="PIIyif7NFRS57mY="; + ciphertexts[12]="QEKXrRWdZ3poJVSp"; + ciphertexts[13]="T3zsAsooHuAnflNsNQ=="; + ciphertexts[14]="qgYtOJjZSIByo0KtOG0="; + ciphertexts[15]="NsEGHGK6Ju6FjD59Byai"; + ciphertexts[16]="1RVIsC0HYoUEycoA_0UL2w=="; + ciphertexts[17]="0xXe6Lsb1tZ79T96AJTT-ps="; + ciphertexts[18]="cVXQCYoA4RV8t1CODXuCS88y"; + ciphertexts[19]="hVf4pd4WP4wPwSyqEXRRkQZSQA=="; + ciphertexts[20]="F6Y9MHwhd1e-bDHhqNSonZbR2Sg="; + ciphertexts[21]="TiMClYbLUdyYweW8IDytU_HD2wTM"; + ciphertexts[22]="tYQtNqz83KXE4eqn6GhAu6ZZ23SqYw=="; + ciphertexts[23]="qjL-dMpiQ2LYgkYT5IfmE1FlN36wHek="; + ciphertexts[24]="cL7HHiOZ9PbkvZ9yrJLiv4HXcw4Nf7y7"; + ciphertexts[25]="k4I-fdR6CyzxOpR_QEG5rnvPB8IbmRnpFg=="; + ciphertexts[26]="7LjCfA1dCMjAVT_O8DpiTQ0G7igwQ1HTUMU="; + ciphertexts[27]="CAtijc6nB-REwAkqimToMn8RC_eZAaJy9Gn4"; + ciphertexts[28]="z8sEB1lDI32wsOkgYbVZ5pxIbpCrha9BmcqxFQ=="; + ciphertexts[29]="2eysfzsfGav0vPRsSnFl8H8fg9dQCT_bSiZwno0="; + ciphertexts[30]="2BBNlF_mtV9TB2jZHHqCAtzkJQFdVKFn7N8YxsI9"; + ciphertexts[31]="9h4-nldHAr77Boks7lPzsi8TwVCIQzSkiJp2xatbGg=="; + ciphertexts[32]="DHTB8bDTXpUIrZ2ZlAujXLi-501NoWUVIEQJLaKCpqQ="; + ciphertexts[33]="E9Av2GgnZg_q5r-JLSzM_ShCu1yPF2VeCaQfPPXSSE4I"; + ciphertexts[34]="UJzEucVBnGEfRNBQ6tvbaro0_I_-mQeJMpU2zQnfFdBuFg=="; + ciphertexts[35]="_p0OYras-Vn2rQ9X-J0dFRnhCfytuTEjheUTU7Ueaf1rIA4="; + ciphertexts[36]="Q0nZXFPJbpx1WZPP-lLPuSGR-pD08B4CAW-6Uf0eEkS05-oM"; + ciphertexts[37]="XeKfieZGc9bPh7nRtCgujF8OY14zbIZSK20Lwg1HTpHi9HfXVQ=="; + + var clientKeyArray = b64.decodeString("dtmbEN1kgN/LmuEoYifaFw=="); + // wrappedKey was "MTpPH3pnLDKihecOci+0W5dk" + var count = 0xFEDCBA09; + var plaintext = "http://www.foobar.com/this?is&some=url"; + + // For every substring of the plaintext, change the count and verify + // that we get what we expect when we encrypt + + for (var i = 0; i < plaintext.length; i++) { + var plaintextArray = b64.arrayifyString(plaintext.substring(0, i)); + var crypted = c.encryptV1(clientKeyArray, + "1", + count + i, + plaintextArray); + G_Assert(z, crypted === ciphertexts[i], + "Generated unexpected ciphertext"); + + // Uncomment to generate + // dump("\nciphertexts[" + i + "]=\"" + crypted + "\";"); + } + + // Cribbed from the MD5 rfc to make sure we're computing plain'ol md5 + // values correctly + // http://www.faqs.org/rfcs/rfc1321.html + var md5texts = [ "", + "a", + "abc", + "message digest", + "abcdefghijklmnopqrstuvwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + ]; + + var md5digests = [ "d41d8cd98f00b204e9800998ecf8427e", + "0cc175b9c0f1b6a831c399e269772661", + "900150983cd24fb0d6963f7d28e17f72", + "f96b697d7cb7938d525a2f31aaf161d0", + "c3fcd3d76192e4007dfb496cca67e13b", + "d174ab98d277d9f5a5611c2c9f419d9f", + "57edf4a22be3c955ac49da2e2107b67a" + ]; + + var expected_mac = []; + expected_mac[0] = "ZOJ6Mk+ccC6R7BwseqCYQQ=="; + expected_mac[1] = "zWM7tvcsuH/MSEviNiRbOA=="; + expected_mac[2] = "ZAUVyls/6ZVN3Np8v3pX3g=="; + expected_mac[3] = "Zq6gF7RkPwKqlicuxrO4mg=="; + expected_mac[4] = "/LOJETSnqSW3q4u1hs/0Pg=="; + expected_mac[5] = "jjOEX7H2uchOznxIGuqzJg=="; + expected_mac[6] = "Tje7aP/Rk/gkSH4he0KMQQ=="; + + // Check plain'ol MD5 + var hasher = new G_CryptoHasher(); + for (var i = 0; i < md5texts.length; i++) { + var computedMac = c.computeMac(md5texts[i], true /* output raw */, [], []); + var hex = hasher.toHex_(computedMac).toLowerCase(); + G_Assert(z, hex == md5digests[i], + "MD5(" + md5texts[i] + ") = " + md5digests[i] + ", not " + hex); + } + + for (var i = 0; i < md5texts.length; i++) { + var computedMac = c.computeMac(md5texts[i], + false /* output Base64 */, + clientKeyArray); + G_Assert(z, computedMac == expected_mac[i], + "Wrong mac generated for " + md5texts[i]); + // Uncomment to generate + // dump("\nexpected_mac[" + i + "] = \"" + computedMac + "\";"); + } + + // Now check if adding line by line is the same as computing over the whole + // thing + var wholeString = md5texts[0] + md5texts[1]; + var wholeStringMac = c.computeMac(wholeString, + false /* output Base64 */, + clientKeyArray); + + expected_mac = "zWM7tvcsuH/MSEviNiRbOA=="; + c.initMac(clientKeyArray); + c.updateMacFromString(md5texts[0]); + c.updateMacFromString(md5texts[1]); + var piecemealMac = c.finishMac(clientKeyArray); + G_Assert(z, piecemealMac == wholeStringMac, + "Computed different values for mac when adding line by line!"); + G_Assert(z, piecemealMac == expected_mac, "Didn't generate expected mac"); + + // Tested also in cheesey-test-frontend.sh and urlcrypto_unittest.cc on + // the server side + expected_mac = "iA5vLUidpXAPwfcAH9+8OQ=="; + var set3data = ""; + for (var i = 1; i <= 3; i++) { + set3data += "+white" + i + ".com\t1\n"; + } + var computedMac = c.computeMac(set3data, false /*Base64*/, clientKeyArray); + G_Assert(z, expected_mac == computedMac, "Expected " + expected_mac, " got " + computedMac); + + removeTestFile(kf); + + G_Debug(z, "PASS"); + } +} diff --git a/mozilla/toolkit/components/protection/content/wireformat.js b/mozilla/toolkit/components/protection/content/wireformat.js new file mode 100644 index 00000000000..1def5bfdbac --- /dev/null +++ b/mozilla/toolkit/components/protection/content/wireformat.js @@ -0,0 +1,776 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Niels Provos (original author) + * Fritz Schneider + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +// A class that serializes and deserializes opaque key/value string to +// string maps to/from maps (trtables). It knows how to create +// trtables from the serialized format, so it also understands +// meta-information like the name of the table and the table's +// version. See docs for the protocol description. +// +// TODO: wireformatreader: if you have multiple updates for one table +// in a call to deserialize, the later ones will be merged +// (all but the last will be ignored). To fix, merge instead +// of replace when you have an existing table, and only do so once. +// TODO must have blank line between successive types -- problem? +// TODO doesn't tolerate blank lines very well +// +// Maybe: These classes could use a LOT more cleanup, but it's not a +// priority at the moment. For example, the tablesData/Known +// maps should be combined into a single object, the parser +// for a given type should be separate from the version info, +// and there should be synchronous interfaces for testing. + + +/** + * A class that knows how to serialize and deserialize meta-information. + * This meta information is the table name and version number, and + * in its serialized form looks like the first line below: + * + * [name-of-table X.Y update?] + * ...key/value pairs to add or delete follow... + * + * + * The X.Y is the version number and the optional "update" token means + * that the table is a differential from the curent table the extension + * has. Its absence means that this is a full, new table. + */ +function PROT_VersionParser(type, opt_major, opt_minor, opt_requireMac) { + this.debugZone = "versionparser"; + this.type = type; + this.major = 0; + this.minor = 0; + + this.badHeader = false; + + // Should the wireformatreader compute a mac? + this.mac = false; + this.macval = ""; + this.macFailed = false; + this.requireMac = !!opt_requireMac; + + this.update = false; + // Used by ListerManager to see if we have read data for this table from + // disk. Once we read a table from disk, we are not going to do so again + // but instead update remotely if necessary. + this.didRead = false; + if (opt_major) + this.major = parseInt(opt_major); + if (opt_minor) + this.minor = parseInt(opt_minor); +} + +/** Import the version information from another VersionParser + * @params version a version parser object + */ +PROT_VersionParser.prototype.ImportVersion = function(version) { + this.major = version.major; + this.minor = version.minor; + + this.mac = version.mac; + this.macFailed = version.macFailed; + this.macval = version.macval; + // Don't set requireMac, since wfr creates vparsers from scratch and doesn't + // know about it +} + +/** + * Creates a string like [goog-white-black 1.1] from internal information + * + * @returns version string + */ +PROT_VersionParser.prototype.toString = function() { + var s = "[" + this.type + " " + this.major + "." + this.minor + "]"; + if (this.mac) + s += "[mac=" + this.macval + "]"; + return s; +} + +/** + * Creates a string like 1:1 from internal information used for + * fetching updates from the server. Called by the listmanager. + * + * @returns version string + */ +PROT_VersionParser.prototype.toUrl = function() { + return this.major + ":" + this.minor; +} + +/** + * Process the old format, [type major.minor [update]] + * + * @returns true if the string could be parsed, false otherwise + */ +PROT_VersionParser.prototype.processOldFormat_ = function(line) { + if (line[0] != '[' || line.slice(-1) != ']') + return false; + + var description = line.slice(1, -1); + + // Get the type name and version number of this table + var tokens = description.split(" "); + this.type = tokens[0]; + var majorminor = tokens[1].split("."); + this.major = parseInt(majorminor[0]); + this.minor = parseInt(majorminor[1]); + if (isNaN(this.major) || isNaN(this.minor)) + return false; + + if (tokens.length >= 3) { + this.update = tokens[2] == "update"; + } + + return true; +} + +/** + * Takes a string like [name-of-table 1.1] and figures out the type + * and corresponding version numbers. + * + * @returns true if the string could be parsed, false otherwise + */ +PROT_VersionParser.prototype.fromString = function(line) { + G_Debug(this, "Calling fromString with line: " + line); + if (line[0] != '[' || line.slice(-1) != ']') + return false; + + // There could be two [][], so take care of it + var secondBracket = line.indexOf('[', 1); + var firstPart = null; + var secondPart = null; + + if (secondBracket != -1) { + firstPart = line.substring(0, secondBracket); + secondPart = line.substring(secondBracket); + G_Debug(this, "First part: " + firstPart + " Second part: " + secondPart); + } else { + firstPart = line; + G_Debug(this, "Old format: " + firstPart); + } + + if (!this.processOldFormat_(firstPart)) + return false; + + if (secondPart && !this.processOptTokens_(secondPart)) + return false; + + return true; +} + +/** + * Process optional tokens + * + * @param line A string [token1=val1 token2=val2...] + * @returns true if the string could be parsed, false otherwise + */ +PROT_VersionParser.prototype.processOptTokens_ = function(line) { + if (line[0] != '[' || line.slice(-1) != ']') + return false; + var description = line.slice(1, -1); + // Get the type name and version number of this table + var tokens = description.split(" "); + + for (var i = 0; i < tokens.length; i++) { + G_Debug(this, "Processing optional token: " + tokens[i]); + var tokenparts = tokens[i].split("="); + switch(tokenparts[0]){ + case "mac": + this.mac = true; + if (tokenparts.length < 2) { + G_Debug(this, "Found mac flag but not mac value!"); + return false; + } + // The mac value may have "=" in it, so we can't just use tokenparts[1]. + // Instead, just take the rest of tokens[i] after the first "=" + this.macval = tokens[i].substr(tokens[i].indexOf("=")+1); + break; + default: + G_Debug(this, "Found unrecognized token: " + tokenparts[0]); + break; + } + } + + return true; +} + + +/** + * A WireFormatWriter can serialize table data + * + * @param threadQueue A thread queue we should run on + * + * @constructor + */ +function PROT_WireFormatWriter(threadQueue) { + this.threadQueue_ = threadQueue; + this.debugZone = "wireformatwriter"; +} + +/** + * Serializes a table to a string. + * + * @param tableData Reference to the table data we should serialize + * + * @param vParser Reference to the version parser/unparser we should use + * + * @param callback Reference to a function we should call when complete. + * The callback will be invoked with a string holding + * the serialized data as an argument + * + * @returns False if the serializer is busy, else true + * + */ +PROT_WireFormatWriter.prototype.serialize = function(tableData, + vParser, + callback) { + if (this.callback_) { + G_Debug(this, "Serializer busy"); + return false; + } + + this.callback_ = callback; + this.current_ = 0; + this.serialized_ = vParser.toString() + "\n"; + var cnt = {}; + this.keyList_ = tableData.getKeys(cnt); + this.tableData_ = tableData; + + this.threadQueue_.addWorker(BindToObject(this.doWorkUnit, this), + BindToObject(this.isComplete, this)); + this.threadQueue_.run(); + + return true; +} + +/** + * Serialize a chunk of data. This is periodically invoked by the + * threadqueue. + */ +PROT_WireFormatWriter.prototype.doWorkUnit = function() { + for (var i = 0; i < 10 && this.current_ < this.keyList_.length; i++) { + var key = this.keyList_[this.current_]; + if (key) { + var value = this.tableData_.findValue(key); + this.serialized_ += "+" + key + "\t" + value + "\n"; + } + this.current_++; + } + + if (this.isComplete()) + this.onComplete(); +} + +/** + * Are we done serializing? Called by the threadqueue to tell when + * to stop running this thread. + * + * @returns Boolean indicating if we're done serializing + */ +PROT_WireFormatWriter.prototype.isComplete = function() { + return this.current_ >= this.keyList_.length; +} + +/** + * When we're done, call the callback + */ +PROT_WireFormatWriter.prototype.onComplete = function() { + this.callback_(this.serialized_); + this.callback_ = null; +} + +/** + * A WireFormatReader can deserialize data. + * + * @constructor + * + * @param threadQueue A global thread queue that we use to schedule our + * work so that we do not take up too much time at one. + * + * @param opt_existingTablesData Optional reference to a map of tables + * into which we should merge the data being deserialized + */ +function PROT_WireFormatReader(threadQueue, opt_existingTablesData) { + this.debugZone = "wireformatreader"; + this.existingTablesData_ = opt_existingTablesData; + this.threadQueue_ = threadQueue; + this.callback_ = null; + + // For mac'ing updates + this.urlCrypto_ = null; + var keyUrl = null; + try { + keyUrl = PROT_GlobalStore.getGetKeyURL(); + } catch (e) { + G_Debug(this, "No key url, disabling mac'ed updates"); + } + if (keyUrl) { + this.urlCrypto_ = new PROT_UrlCrypto(); + } +} + +/** + * Starts a thread to process the input data. + * + * @param tableUpdate A string that contains the data for updating the + * tables that we know about. + * + * @param callback A user specificed callback that is being executed + * when data processing completes. The callback is provided + * with two arguments: tablesKnown and tablesData + * + * @returns true is new data is being process, or false on failure. + */ +PROT_WireFormatReader.prototype.deserialize = function(tableUpdate, callback) { + this.tableUpdate_ = tableUpdate; + this.tablesKnown_ = {}; // version parsers + this.tablesData_ = {}; // TRTables + + if (this.callback_ != null) { + G_Debug(this, "previous deserialize is still running"); + return false; + } + + this.callback_ = callback; + this.offset_ = 0; + this.resetTableState_(); + + // On empty data, we just invoke the callback directly + if (!this.tableUpdate_ || !this.tableUpdate_.length) { + G_Debug(this, "No data. Calling back."); + this.onComplete(); + return true; + } + + this.threadQueue_.addWorker(BindToObject(this.processLine_, this), + BindToObject(this.isComplete, this)); + this.threadQueue_.run(); + + return true; +} + +/** + * Resets the per table state + */ +PROT_WireFormatReader.prototype.resetTableState_ = function() { + this.vParser_ = null; + this.insert_ = 0; + this.remove_ = 0; +} + +/** + * Initalizes our state to process a new table. + * NOTE: For performance reasons, we might have drive the replacement of + * the table via a thread. + * + * @param line The input line that contains the table name and version + */ +PROT_WireFormatReader.prototype.processNewTable_ = function(line) { + this.vParser_ = new PROT_VersionParser("something"); + + // If there's an error, give up + if (!this.vParser_.fromString(line)) { + G_Debug(this, "Error in table header, skipping"); + this.vParser_ = null; + return; + } + + G_Debug(this, "processNewTable: " + this.vParser_.type + ": " + + this.vParser_.major + ":" + this.vParser_.minor + " update=" + + this.vParser_.update + " mac=" + this.vParser_.mac); + + // Create temporary table. PROT_TRTable() blows up if it doesn't like its + // name, so mask and just set vParser to null + try { + this.tablesData_[this.vParser_.type] = + newProtectionTable(this.vParser_.type); + } catch(e) { + G_Debug(this, + "Unable to initialize new TRTable, because of exception: " + e); + this.vParser_ = null; + return; + } + if (this.vParser_.update && this.existingTablesData_ && + this.existingTablesData_[this.vParser_.type]) { + // If we update an existing table, we need to copy the old table + // data from the pre-existing tables + this.tablesData_[this.vParser_.type].replace( + this.existingTablesData_[this.vParser_.type]); + } + G_Debug(this, "processNewTable done"); +} + +/** + * Updates the contents of our temporary table. Exceptions should not + * be masked here -- they're caught at a higher level. + * + * @param line The input line that contains the new data + */ +PROT_WireFormatReader.prototype.processUpdateTable_ = function(line) { + // Regular update to the current version + var tokens = line.split("\t"); + var operation = tokens[0][0]; + var key = tokens[0].slice(1); + var value = tokens[1]; + + if (operation == "+") { + this.tablesData_[this.vParser_.type].insert(key, value); + this.insert_++; + } else { + this.tablesData_[this.vParser_.type].erase(key); + this.remove_++; + } +} + +/** + * Finish processing a table + */ +PROT_WireFormatReader.prototype.endTable_ = function() { + G_Debug(this, + "Finished: " + this.vParser_.type + " +" + + this.insert_ + " -" + this.remove_ + " mac'ing: " + this.vParser_.mac); + + if (this.vParser_.mac) { + var mac = this.urlCrypto_.finishMac(); + if (mac != this.vParser_.macval) { + G_Debug(this, "Mac didn't match! (" + mac + " != " + + this.vParser_.macval + ")"); + this.vParser_.macFailed = true; + } else { + G_Debug(this, "Computed mac matched sent mac: " + this.vParser_.macval); + } + } + + // Rollback if the mac failed. + if (this.vParser_.mac && this.vParser_.macFailed) { + this.tablesData_[this.vParser_.type] = undefined; + this.tablesKnown_[this.vParser_.type] = undefined; + G_Debug(this, "throwing away " + this.vParser_.type); + } else { + this.tablesKnown_[this.vParser_.type] = this.vParser_; + } + + this.resetTableState_(); +} + +/** + * Processes a chunk of data. TODO(MC): Make it so the mac is computed from a + * saved begin and end offset instead of computing line by line. + */ +PROT_WireFormatReader.prototype.processLine_ = function() { + //G_Debug(this, '> processline'); + for (var count = 0; + count < 5 && this.offset_ < this.tableUpdate_.length; count++) { + var newOffset = this.tableUpdate_.indexOf("\n", this.offset_); + var line = ""; + if (newOffset == -1) { + this.offset_ = this.tableUpdate_.length; + } else { + line = this.tableUpdate_.slice(this.offset_, newOffset); + this.offset_ = newOffset + 1; + } + + // Ignore empty lines if we currently do not have a table + if (!this.vParser_ && (!line || line[0] != '[')) { + continue; + } + + if (!line) { + // End of one table - pop the results + this.endTable_(); + } else if (line[0] == '[' && line.slice(-1) == ']') { + // processNewTable doesn't instantiate this.vParser_ in case of malformed + // headers, so the rest of the table lines will get skipped + this.processNewTable_(line); + + if (this.vParser_ && this.vParser_.mac) { + this.urlCrypto_.initMac(); + } + } else { + if (!this.vParser_) { + G_Debug(this, "Ignoring: " + line); + continue; + } + + // Now we try to read a data line. However the table could've + // been corrupted on disk (e.g., the browser suddenly quit while + // we were in the middle of writing a line). If so, + // porcessUpdateTable() will throw. In this case we want to + // resynch the whole table, so we skip this line and then + // explicitly set the table's minor version to -1 (the lowest + // possible -- not 0, which means the table is local), causing a + // full update the next time we ask for it. + // + // We ignore the case where we wrote an incomplete but malformed + // table -- it fixes itself over time as the missing keys become + // less relevant. + + try { + this.processUpdateTable_(line); + // Compute the mac over all the next lines until we finish the table + // TODO: This is ugly! line doesn't get the final '\n', so add it back. + // We could save the original line back up top, and use that instead, + // but that's yet more copying... + if (this.vParser_.mac) { + this.urlCrypto_.updateMacFromString(line + "\n"); + } + } catch(e) { + G_Debug(this, "MALFORMED TABLE LINE: [" + line + "]\n" + + "Skipping this line, and resetting table " + + this.vParser_.type + " to version -1.\n" + + "(This as a result of exception: " + e + ")"); + this.vParser_.minor = "-1"; + } + } + } + + // If the table we're reading is the last, then the for loop will + // fail, causing the table finish logic to be skipped. So here + // ensure that we finish up whatever table we're working on. + + if (this.vParser_ && this.offset_ >= this.tableUpdate_.length) { + G_Debug(this, + "Finished (final table): " + this.vParser_.type + " +" + + this.insert_ + " -" + this.remove_); + this.endTable_(); + } + + if (this.isComplete()) this.onComplete(); + //G_Debug(this, "< processline"); +} + +/** + * Returns true if all input data has been processed + */ +PROT_WireFormatReader.prototype.isComplete = function() { + return this.offset_ >= this.tableUpdate_.length; +} + +/** + * Notifies our caller of completion. + */ +PROT_WireFormatReader.prototype.onComplete = function() { + G_Debug(this, "Processing complete. Executing callback."); + this.callback_(this.tablesKnown_, this.tablesData_); + this.callback_ = null; +} + + +function TEST_PROT_WireFormat() { + if (G_GDEBUG) { + // Sorry, this is incredibly ugly. What we need is continuations -- each + // unittest that passes invokes the next. + + var z = "wireformat UNITTEST"; + G_debugService.enableZone(z); + G_Debug(z, "Starting"); + + function testMalformedTables() { + G_Debug(z, "Testing malformed tables..."); + + var wfr = new PROT_WireFormatReader(new TH_ThreadQueue()); + + // Damn these unittests are ugly. Ugh. Now test handling corrupt tables + var data = + "[test1-black-url 1.1]\n" + + "+foo1\tbar\n" + + "+foo2\n" + // Malformed + "+foo3\tbar\n" + + "+foo4\tbar\n" + + "\n" + + "[test2-black-url 1.2]\n" + + "+foo1\tbar\n" + + "+foo2\tbar\n" + + "+foo3\tbar\n" + + "+foo4\tbar\n" + + "\n" + + "[test3-black-url 1.3]\n" + + "+foo1\tbar\n" + + "+foo2\tbar\n" + + "+foo3\tbar\n" + + "+foo4\n" + // Malformed + "\n" + + "[test4-black-url 1.4]\n" + + "+foo1\tbar\n" + + "+foo2\tbar\n" + + "+foo3\tbar\n" + + "+foo4\tbar\n" + + "\n" + + "[test4-badheader-url 1.asfd dfui]\n" + // Malformed header + "+foo1\tbar\n" + + "+foo2\tbar\n" + + "+foo3\tbar\n" + + "+foo4\tbar\n"; + "\n" + + "[test4-bad-name-url 1.asfd dfui]\n" + // Malformed header + "+foo1\tbar\n" + + "+foo2\tbar\n" + + "+foo3\tbar\n" + + "+foo4\tbar\n"; + + function malformedcb(tablesKnown, tablesData) { + + // Table has malformed data + G_Assert(z, tablesKnown["test1-black-url"].minor == "-1", + "test table 1 didn't get reset"); + G_Assert(z, !!tablesData["test1-black-url"].find("foo1"), + "test table 1 didn't set keys before the error"); + G_Assert(z, !!tablesData["test1-black-url"].find("foo3"), + "test table 1 didn't set keys after the error"); + + // Table should be good + G_Assert(z, tablesKnown["test2-black-url"].minor == "2", + "test table 1 didnt get correct version number"); + G_Assert(z, !!tablesData["test2-black-url"].find("foo4"), + "test table 2 didnt parse properly"); + + // Table is malformed + G_Assert(z, tablesKnown["test3-black-url"].minor == "-1", + "test table 3 didn't get reset"); + G_Assert(z, !tablesData["test3-black-url"].find("foo4"), + "test table 3 somehow has its malformed line?"); + + // Table should be good + G_Assert(z, tablesKnown["test4-black-url"].minor == "4", + "test table 4 didn't get correct version number"); + G_Assert(z, !!tablesData["test2-black-url"].find("foo4"), + "test table 4 didnt parse properly"); + + // Table is malformed and should be unknown + G_Assert(z, !tablesKnown["test4-badheader-url"], + "test table header should't be known"); + + // Table is malformed and should be unknown + G_Assert(z, !tablesKnown["test4-bad-name-url"], + "test table header should't be known"); + + G_Debug(z, "PASSED"); + }; + + wfr.deserialize(data, malformedcb); + }; + + var testTablesData = {}; + + var tableName = "test-black-url"; + var data1 = "[" + tableName + " 1.5]\n"; + for (var i = 0; i < 100; i++) + data1 += "+http://exists" + i + "\t1\n"; + data1 += "-http://exists50\t1\n"; + data1 += "-http://exists666\t1\n"; + + var data2 = "[" + tableName + " 1.7 update]\n"; + for (var i = 0; i < 100; i++) + data2 += "-http://exists" + i + "\t1\n"; + + var set3Name = "test-white-domain"; + var set3data = ""; + // Use this string since it's the same in the update server unittest in + // /firefox/security/scripts/test/cheese/cheesey something.sh + for (var i = 1; i <= 3; i++) { + set3data += "+white" + i + ".com\t1\n"; + } + var urlCrypto = new PROT_UrlCrypto(); + var computedMac = urlCrypto.computeMac(set3data); + + function data1cb(tablesKnown, tablesData) { + G_Assert(z, + tablesData[tableName] != null, + "Didn't get our table back"); + + for (var i = 0; i < 100; i++) + if (i != 50) + G_Assert(z, + tablesData[tableName].find("http://exists" + i) == "1", + "Item addition broken"); + + G_Assert(z, + !tablesData[tableName].find("http://exists50"), + "Item removal broken"); + G_Assert(z, + !tablesData[tableName].find("http://exists666"), + "Non-existent item"); + + G_Assert(z, tablesKnown[tableName].major == "1", "Major parsing broke"); + G_Assert(z, tablesKnown[tableName].minor == "5", "Major parsing broke"); + + var wfr2 = new PROT_WireFormatReader(new TH_ThreadQueue(), tablesData); + wfr2.deserialize(data2, data2cb); + }; + + function data2cb(tablesKnown, tablesData) { + for (var i = 0; i < 100; i++) + G_Assert(z, + !tablesData[tableName].find("http://exists" + i), + "Tables merge broken"); + + G_Assert(z, tablesKnown[tableName].major == "1", "Major parsing broke"); + G_Assert(z, tablesKnown[tableName].minor == "7", "Major parsing broke"); + + var wfr3 = new PROT_WireFormatReader(new TH_ThreadQueue(), tablesData); + var data3 = "[test-white-domain 1.1][mac=" + computedMac + "]\n" + + set3data; + wfr3.deserialize(data3, data3cb); + }; + + // Test the MAC stuff + function data3cb(tablesKnown, tablesData) { + G_Assert(z, tablesData["test-white-domain"] != null, + "Didn't get our table back though the mac was correct"); + + // Now do the same thing with the wrong mac + computedMac = "012" + computedMac.substr(3); + var data4 = "[test-foo-domain 1.1][mac=" + computedMac + "]\n" + + set3data; + var wfr4 = new PROT_WireFormatReader(new TH_ThreadQueue(), tablesData); + wfr4.deserialize(data4, data4cb); + } + + // Test the MAC stuff + function data4cb(tablesKnown, tablesData) { + G_Assert(z, !tablesKnown["test-foo-domain"], + "Deserialized though our mac was wrong"); + + G_Assert(z, !tablesData["test-foo-domain"], + "Deserialized though our mac was wrong"); + + testMalformedTables(); + + } + var wfr = new PROT_WireFormatReader(new TH_ThreadQueue()); + wfr.deserialize(data1, data1cb); + } +} diff --git a/mozilla/toolkit/components/protection/content/xml-fetcher.js b/mozilla/toolkit/components/protection/content/xml-fetcher.js new file mode 100644 index 00000000000..7294d522d14 --- /dev/null +++ b/mozilla/toolkit/components/protection/content/xml-fetcher.js @@ -0,0 +1,200 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Google Safe Browsing. + * + * The Initial Developer of the Original Code is Google Inc. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fritz Schneider (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// A simple class that encapsulates a request. You'll notice the +// style here is different from the rest of the extension; that's +// because this was re-used from really old code we had. At some +// point it might be nice to replace this with something better +// (e.g., something that has explicit onerror handler, ability +// to set headers, and so on). +// +// The only interesting thing here is its ability to strip cookies +// from the request. + +/** + * Because we might be in a component, we can't just assume that + * XMLHttpRequest exists. So we use this tiny class to wrap the XPCOM + * version. + * + * @constructor + */ +function PROT_XMLHttpRequest() { + var Cc = Components.classes; + var Ci = Components.interfaces; + var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + // Need the following so we get onerror/load/progresschange + request.QueryInterface(Ci.nsIJSXMLHttpRequest); + return request; +} + +/** + * A helper class that does HTTP GETs and calls back a function with + * the content it receives. Asynchronous, so uses a closure for the + * callback. + * + * @param opt_stripCookies Boolean indicating whether we should strip + * cookies from this request + * + * @constructor + */ +function PROT_XMLFetcher(opt_stripCookies) { + this.debugZone = "xmlfetcher"; + this._request = new PROT_XMLHttpRequest(); + this._stripCookies = !!opt_stripCookies; +} + +PROT_XMLFetcher.prototype = { + /** + * Function that will be called back upon fetch completion. + */ + _callback: null, + + + /** + * Fetches some content. + * + * @param page URL to fetch + * @param callback Function to call back when complete. + */ + get: function(page, callback) { + this._request.abort(); // abort() is asynchronous, so + this._request = new PROT_XMLHttpRequest(); + this._callback = callback; + var asynchronous = true; + this._request.open("GET", page, asynchronous); + + if (this._stripCookies) + new PROT_CookieStripper(this._request.channel); + + // Create a closure + var self = this; + this._request.onreadystatechange = function() { + self.readyStateChange(self); + } + + this._request.send(null); + }, + + /** + * Called periodically by the request to indicate some state change. 4 + * means content has been received. + */ + readyStateChange: function(fetcher) { + if (fetcher._request.readyState != 4) // TODO: check status code 200 + return; + + // We occasionally get an NS_ERROR_NOT_AVAILABLE (it doesn't have + // headers) when we try to read the response. Mask the exception + // by returning null response. + // TODO maybe masking this should be an option? + try { + G_Debug(this, "xml fetch status code: \"" + + fetcher._request.status + "\""); + if (fetcher._callback) + fetcher._callback(fetcher._request.responseText); + } catch(e) { + G_Debug(this, "Caught exception trying to read xmlhttprequest " + + "status/response."); + G_Debug(this, e); + if (fetcher._callback) + fetcher._callback(null); + } + } +}; + + +/** + * This class knows how to strip cookies from an HTTP request. It + * listens for http-on-modify-request, and modifies the request + * accordingly. We can't do this using xmlhttprequest.setHeader() or + * nsIChannel.setRequestHeader() before send()ing because the cookie + * service is called after send(). + * + * @param channel nsIChannel in which the request is happening + * @constructor + */ +function PROT_CookieStripper(channel) { + this.debugZone = "cookiestripper"; + this.topic_ = "http-on-modify-request"; + this.channel_ = channel; + + var Cc = Components.classes; + var Ci = Components.interfaces; + this.observerService_ = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + this.observerService_.addObserver(this, this.topic_, false); + + // If the request doesn't issue, don't hang around forever + var twentySeconds = 20 * 1000; + this.alarm_ = new G_Alarm(BindToObject(this.stopObserving, this), + twentySeconds); +} + +/** + * Invoked by the observerservice. See nsIObserve. + */ +PROT_CookieStripper.prototype.observe = function(subject, topic, data) { + if (topic != this.topic_ || subject != this.channel_) + return; + + G_Debug(this, "Stripping cookies for channel."); + + this.channel_.QueryInterface(Components.interfaces.nsIHttpChannel); + this.channel_.setRequestHeader("Cookie", "", false /* replace, not add */); + this.alarm_.cancel(); + this.stopObserving(); +} + +/** + * Remove us from the observerservice + */ +PROT_CookieStripper.prototype.stopObserving = function() { + G_Debug(this, "Removing observer"); + this.observerService_.removeObserver(this, this.topic_); + this.channel_ = this.alarm_ = this.observerService_ = null; +} + +/** + * XPCOM cruft + */ +PROT_CookieStripper.prototype.QueryInterface = function(iid) { + var Ci = Components.interfaces; + if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIObserve)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; +} + diff --git a/mozilla/toolkit/components/protection/jar.mn b/mozilla/toolkit/components/protection/jar.mn new file mode 100644 index 00000000000..1f14cf94944 --- /dev/null +++ b/mozilla/toolkit/components/protection/jar.mn @@ -0,0 +1,27 @@ +toolkit.jar: ++ content/global/protection/application.js (content/application.js) ++ content/global/protection/enchash-decrypter.js (content/enchash-decrypter.js) ++ content/global/protection/globalstore.js (content/globalstore.js) ++ content/global/protection/listmanager.js (content/listmanager.js) ++ content/global/protection/list-warden.js (content/list-warden.js) ++ content/global/protection/map.js (content/map.js) ++ content/global/protection/url-canonicalizer.js (content/url-canonicalizer.js) ++ content/global/protection/url-crypto.js (content/url-crypto.js) ++ content/global/protection/url-crypto-key-manager.js (content/url-crypto-key-manager.js) ++ content/global/protection/wireformat.js (content/wireformat.js) ++ content/global/protection/xml-fetcher.js (content/xml-fetcher.js) +# ++ content/global/protection/js/arc4.js (content/js/arc4.js) ++ content/global/protection/js/lang.js (content/js/lang.js) ++ content/global/protection/js/thread-queue.js (content/js/thread-queue.js) +# ++ content/global/protection/moz/alarm.js (content/moz/alarm.js) ++ content/global/protection/moz/base64.js (content/moz/base64.js) ++ content/global/protection/moz/cryptohasher.js (content/moz/cryptohasher.js) ++ content/global/protection/moz/debug.js (content/moz/debug.js) ++ content/global/protection/moz/filesystem.js (content/moz/filesystem.js) ++ content/global/protection/moz/lang.js (content/moz/lang.js) ++ content/global/protection/moz/objectsafemap.js (content/moz/objectsafemap.js) ++ content/global/protection/moz/observer.js (content/moz/observer.js) ++ content/global/protection/moz/preferences.js (content/moz/preferences.js) ++ content/global/protection/moz/protocol4.js (content/moz/protocol4.js) diff --git a/mozilla/toolkit/components/protection/public/Makefile.in b/mozilla/toolkit/components/protection/public/Makefile.in new file mode 100644 index 00000000000..1c777a68654 --- /dev/null +++ b/mozilla/toolkit/components/protection/public/Makefile.in @@ -0,0 +1,15 @@ +DEPTH=../../../.. +topsrcdir=@top_srcdir@ +srcdir=@srcdir@ +VPATH=@srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = protection +XPIDL_MODULE = protection + +XPIDLSRCS = nsIProtectionTable.idl \ + nsIProtectionListManager.idl \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/mozilla/toolkit/components/protection/public/nsIProtectionListManager.idl b/mozilla/toolkit/components/protection/public/nsIProtectionListManager.idl new file mode 100644 index 00000000000..2f2c9b5bf17 --- /dev/null +++ b/mozilla/toolkit/components/protection/public/nsIProtectionListManager.idl @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +/** + * Interface for a class that manages updates of multiple nsIProtectionTables. + */ + +interface nsIFile; + +[scriptable, uuid(e1a80418-1bf9-4bd7-a40d-94d549c24955)] +interface nsIProtectionListManager : nsISupports +{ + /** + * Set the directory to read/write tables to. + */ + void setAppDir(in nsIFile appDir); + + /** + * Add a table to the list of tables we are managing. The name is a + * string of the format provider_name-semantic_type-table_type. For + * example, goog-white-enchash or goog-black-url. + */ + boolean registerTable(in string tableName, + in boolean requireMac); + + /** + * For each table that is enabled, check for updates during + * during the scheduled interval. + */ + void startUpdateChecker(); + + /** + * Stop checking for all updates. + */ + void stopUpdateChecker(); + + /** + * Lookup a key in a table. Should not raise exceptions. + */ + boolean safeLookup(in string tableName, in string key); + + /** + * Insert a key in a table. Should not raise exceptions. + */ + boolean safeInsert(in string tableName, in string key, in string value); + + /** + * Remove a key from a table. Should not raise exceptions. + */ + boolean safeErase(in string tableName, in string key); +}; diff --git a/mozilla/toolkit/components/protection/public/nsIProtectionTable.idl b/mozilla/toolkit/components/protection/public/nsIProtectionTable.idl new file mode 100644 index 00000000000..92ff04e7dc6 --- /dev/null +++ b/mozilla/toolkit/components/protection/public/nsIProtectionTable.idl @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "nsISupports.idl" + +// A map that contains a string keys mapped to string values. +// TODO: maybe nsISerializeable to handle reading/writing from disk? + +[scriptable, uuid(fd1f8334-1859-472d-b01f-4ac6b1121ce4)] +interface nsIProtectionTable : nsISupports +{ + readonly attribute long size; + + /** + * The name used to identify this table + */ + attribute string name; + + /** + * Set to false if we don't want to update this table. + */ + attribute boolean needsUpdate; + + /** + * In the simple case, find just looks up the string in the + * table and returns true if it is found. However, it could + * do something more complex (e.g., canonicalize the url). + */ + boolean find(in string key); + + /** + * In the simple case, key is a url and value is 1. However, + * value could be more complicated (e.g., hashed value based + * on site domain). + */ + void insert(in string key, in string value); + + boolean erase(in string key); + + // TEMPORARY: used to help serialize the table to disk. May be removed. + void getKeys(out unsigned long size, + [retval,array,size_is(size)] out string keys); + + // TEMPORARY: used to help serialize the table to disk. May be removed. + void findValue(in string key); + + // TODO: merge data + // void replace(in nsIProtectionTable); +}; diff --git a/mozilla/toolkit/components/protection/src/Makefile.in b/mozilla/toolkit/components/protection/src/Makefile.in new file mode 100644 index 00000000000..26c8bd94ea3 --- /dev/null +++ b/mozilla/toolkit/components/protection/src/Makefile.in @@ -0,0 +1,16 @@ +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +# EXTRA_COMPONENTS installs components written in JS to dist/bin/components +EXTRA_COMPONENTS = protectionTableUrl.js \ + protectionTableDomain.js \ + protectionTableSite.js \ + protectionTableEnchash.js \ + protectionListManager.js \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/mozilla/toolkit/components/protection/src/protectionListManager.js b/mozilla/toolkit/components/protection/src/protectionListManager.js new file mode 100644 index 00000000000..f96423b80b4 --- /dev/null +++ b/mozilla/toolkit/components/protection/src/protectionListManager.js @@ -0,0 +1,85 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const G_GDEBUG = true; + +// Use subscript loader to load files. The files in ../content get mapped +// to chrome://global/content/protection/. Order matters if one file depends +// on another file during initialization. +const LIB_FILES = [ + "chrome://global/content/protection/js/arc4.js", + "chrome://global/content/protection/js/lang.js", + "chrome://global/content/protection/js/thread-queue.js", + + "chrome://global/content/protection/moz/preferences.js", + "chrome://global/content/protection/moz/filesystem.js", + "chrome://global/content/protection/moz/debug.js", // dep js/lang.js moz/prefs.js moz/filesystem.js + "chrome://global/content/protection/moz/alarm.js", + "chrome://global/content/protection/moz/base64.js", + "chrome://global/content/protection/moz/cryptohasher.js", + "chrome://global/content/protection/moz/lang.js", + "chrome://global/content/protection/moz/observer.js", + "chrome://global/content/protection/moz/protocol4.js", + + "chrome://global/content/protection/application.js", + "chrome://global/content/protection/globalstore.js", + "chrome://global/content/protection/listmanager.js", + "chrome://global/content/protection/url-crypto.js", + "chrome://global/content/protection/url-crypto-key-manager.js", // dep url-crypto.js + "chrome://global/content/protection/wireformat.js", + "chrome://global/content/protection/xml-fetcher.js", +]; + +for (var i = 0, libFile; libFile = LIB_FILES[i]; ++i) { + dump('*** loading subscript ' + libFile + '\n'); + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript(libFile); +} + +// Module object +function ProtectionListManagerMod() { + this.firstTime = true; + this.cid = Components.ID("{ca168834-cc00-48f9-b83c-fd018e58cae3}"); + this.progid = "@mozilla.org/protection/protectionlistmanager;1"; +} + +ProtectionListManagerMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) { + if (this.firstTime) { + this.firstTime = false; + throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN; + } + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + compMgr.registerFactoryLocation(this.cid, + "Protection List Manager Module", + this.progid, + fileSpec, + loc, + type); +}; + +ProtectionListManagerMod.prototype.getClassObject = function(compMgr, cid, iid) { + if (!cid.equals(this.cid)) + throw Components.results.NS_ERROR_NO_INTERFACE; + if (!iid.equals(Ci.nsIFactory)) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + return this.factory; +} + +ProtectionListManagerMod.prototype.canUnload = function(compMgr) { + return true; +} + +ProtectionListManagerMod.prototype.factory = { + createInstance: function(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return (new PROT_ListManager()).QueryInterface(iid); + } +}; + +var ListManagerModInst = new ProtectionListManagerMod(); + +function NSGetModule(compMgr, fileSpec) { + return ListManagerModInst; +} diff --git a/mozilla/toolkit/components/protection/src/protectionTableDomain.js b/mozilla/toolkit/components/protection/src/protectionTableDomain.js new file mode 100644 index 00000000000..b8166c26908 --- /dev/null +++ b/mozilla/toolkit/components/protection/src/protectionTableDomain.js @@ -0,0 +1,102 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const G_GDEBUG = false; + +// Use subscript loader to load files. The files in ../content get mapped +// to chrome://global/content/protection/. Order matters if one file depends +// on another file during initialization. +const LIB_FILES = [ + "chrome://global/content/protection/js/lang.js", + + "chrome://global/content/protection/moz/preferences.js", + "chrome://global/content/protection/moz/filesystem.js", + "chrome://global/content/protection/moz/debug.js", // req js/lang.js moz/prefs.js moz/filesystem.js + + "chrome://global/content/protection/map.js", +]; + +for (var i = 0, libFile; libFile = LIB_FILES[i]; ++i) { + dump('*** loading subscript ' + libFile + '\n'); + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript(libFile); +} + +function ProtectionTableDomain() { + G_Map.call(this); + this.debugZone = "trtable-domain"; +} + +ProtectionTableDomain.inherits(G_Map); + +/** + * Look up a URL in a domain table + * + * @returns Boolean true if the url domain is in the table + */ +ProtectionTableDomain.prototype.find = function(url) { + var urlObj = Cc["@mozilla.org/network/standard-url;1"] + .createInstance(Ci.nsIURL); + urlObj.spec = url; + var host = urlObj.host; + var components = host.split("."); + + // We don't have a good way map from hosts to domains, so we instead try + // each possibility. Could probably optimize to start at the second dot? + for (var i = 0; i < components.length - 1; i++) { + host = components.slice(i).join("."); + var val = this.find_(host); + if (val) + return true; + } + return false; +} + + +// Module object +function ProtectionTableDomainMod() { + this.firstTime = true; + this.cid = Components.ID("{3b5004c6-3fcd-4b12-b311-a4dfbeaf27aa}"); + this.progid = "@mozilla.org/protection/protectiontable;1?type=domain"; +} + +ProtectionTableDomainMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) { + if (this.firstTime) { + this.firstTime = false; + throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN; + } + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + compMgr.registerFactoryLocation(this.cid, + "Protection Table Domain Module", + this.progid, + fileSpec, + loc, + type); +}; + +ProtectionTableDomainMod.prototype.getClassObject = function(compMgr, cid, iid) { + if (!cid.equals(this.cid)) + throw Components.results.NS_ERROR_NO_INTERFACE; + if (!iid.equals(Ci.nsIFactory)) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + return this.factory; +} + +ProtectionTableDomainMod.prototype.canUnload = function(compMgr) { + return true; +} + +ProtectionTableDomainMod.prototype.factory = { + createInstance: function(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return (new ProtectionTableDomain()).QueryInterface(iid); + } +}; + +var DomainModInst = new ProtectionTableDomainMod(); + +function NSGetModule(compMgr, fileSpec) { + return DomainModInst; +} diff --git a/mozilla/toolkit/components/protection/src/protectionTableEnchash.js b/mozilla/toolkit/components/protection/src/protectionTableEnchash.js new file mode 100644 index 00000000000..b1ae2297c17 --- /dev/null +++ b/mozilla/toolkit/components/protection/src/protectionTableEnchash.js @@ -0,0 +1,118 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const G_GDEBUG = false; + +// Use subscript loader to load files. The files in ../content get mapped +// to chrome://global/content/protection/. Order matters if one file depends +// on another file during initialization. +const LIB_FILES = [ + "chrome://global/content/protection/js/arc4.js", + "chrome://global/content/protection/js/lang.js", + + "chrome://global/content/protection/moz/preferences.js", + "chrome://global/content/protection/moz/filesystem.js", + "chrome://global/content/protection/moz/debug.js", // req js/lang.js moz/prefs.js moz/filesystem.js + + "chrome://global/content/protection/map.js", + "chrome://global/content/protection/moz/base64.js", + "chrome://global/content/protection/moz/cryptohasher.js", + "chrome://global/content/protection/enchash-decrypter.js", +]; + +for (var i = 0, libFile; libFile = LIB_FILES[i]; ++i) { + dump('*** loading subscript ' + libFile + '\n'); + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript(libFile); +} + +function ProtectionTableEnchash() { + G_Map.call(this); + this.debugZone = "trtable-enchash"; + this.enchashDecrypter_ = new PROT_EnchashDecrypter(); +} + +ProtectionTableEnchash.inherits(G_Map); + +/** + * Look up a URL in an enchashDB + * + * @returns Boolean indicating whether the URL matches the regular + * expression contained in the table value + */ +ProtectionTableEnchash.prototype.find = function(url) { + var host = this.enchashDecrypter_.getCanonicalHost(url); + + for (var i = 0; i < PROT_EnchashDecrypter.MAX_DOTS + 1; i++) { + var key = this.enchashDecrypter_.getLookupKey(host); + + var encrypted = this.find_(key); + if (encrypted) { + G_Debug(this, "Enchash DB has host " + host); + + // We have encrypted regular expressions for this host. Let's + // decrypt them and see if we have a match. + var decrypted = this.enchashDecrypter_.decryptData(encrypted, host); + var res = this.enchashDecrypter_.parseRegExps(decrypted); + for (var j = 0; j < res.length; j++) { + if (res[j].test(url)) + return true; + } + } + + var index = host.indexOf("."); + if (index == -1) + break; + host = host.substring(index + 1); + } + return false; +} + + +// Module object +function ProtectionTableEnchashMod() { + this.firstTime = true; + this.cid = Components.ID("{04f15d1d-2db8-4b8e-91d7-82f30308b434}"); + this.progid = "@mozilla.org/protection/protectiontable;1?type=enchash"; +} + +ProtectionTableEnchashMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) { + if (this.firstTime) { + this.firstTime = false; + throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN; + } + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + compMgr.registerFactoryLocation(this.cid, + "Protection Table Enchash Module", + this.progid, + fileSpec, + loc, + type); +}; + +ProtectionTableEnchashMod.prototype.getClassObject = function(compMgr, cid, iid) { + if (!cid.equals(this.cid)) + throw Components.results.NS_ERROR_NO_INTERFACE; + if (!iid.equals(Ci.nsIFactory)) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + return this.factory; +} + +ProtectionTableEnchashMod.prototype.canUnload = function(compMgr) { + return true; +} + +ProtectionTableEnchashMod.prototype.factory = { + createInstance: function(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return (new ProtectionTableEnchash()).QueryInterface(iid); + } +}; + +var EnchashModInst = new ProtectionTableEnchashMod(); + +function NSGetModule(compMgr, fileSpec) { + return EnchashModInst; +} diff --git a/mozilla/toolkit/components/protection/src/protectionTableSite.js b/mozilla/toolkit/components/protection/src/protectionTableSite.js new file mode 100644 index 00000000000..85dc3ab9e63 --- /dev/null +++ b/mozilla/toolkit/components/protection/src/protectionTableSite.js @@ -0,0 +1,118 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const G_GDEBUG = false; + +// Use subscript loader to load files. The files in ../content get mapped +// to chrome://global/content/protection/. Order matters if one file depends +// on another file during initialization. +const LIB_FILES = [ + "chrome://global/content/protection/js/lang.js", + + "chrome://global/content/protection/moz/preferences.js", + "chrome://global/content/protection/moz/filesystem.js", + "chrome://global/content/protection/moz/debug.js", // req js/lang.js moz/prefs.js moz/filesystem.js + + "chrome://global/content/protection/map.js", +]; + +for (var i = 0, libFile; libFile = LIB_FILES[i]; ++i) { + dump('*** loading subscript ' + libFile + '\n'); + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript(libFile); +} + +function ProtectionTableSite() { + G_Map.call(this); + this.debugZone = "trtable-site"; + this.ioService_ = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); +} + +ProtectionTableSite.inherits(G_Map); + +/** + * Look up a URL in a site DB + * + * @returns Boolean true if URL matches in table, we normalize based on + * part of the path + */ +ProtectionTableSite.prototype.find = function(url) { + var nsIURI = this.ioService_.newURI(url, null, null); + var host = nsIURI.asciiHost; + var hostComponents = host.split("."); + + var path = nsIURI.path; + var pathComponents = path.split("/"); + + // We don't have a good way to convert a fully specified URL into a + // site. We try host name components with or without the first + // path component. + for (var i = 0; i < hostComponents.length - 1; i++) { + host = hostComponents.slice(i).join(".") + "/"; + var val = this.find_(host); + G_Debug(this, "Checking: " + host + " : " + val); + if (val) + return true; + + // The path starts with a "/", so we are interested in the second path + // component if it is available + if (pathComponents.length >= 2) { + host = host + pathComponents[1] + "/"; + var val = this.find_(host); + G_Debug(this, "Checking: " + host + " : " + val); + if (val) + return true; + } + } + + return false; +} + +// Module object +function ProtectionTableSiteMod() { + this.firstTime = true; + this.cid = Components.ID("{1e48f217-34e4-44a3-9b4a-9b66ed0a1201}"); + this.progid = "@mozilla.org/protection/protectiontable;1?type=site"; +} + +ProtectionTableSiteMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) { + if (this.firstTime) { + this.firstTime = false; + throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN; + } + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + compMgr.registerFactoryLocation(this.cid, + "Protection Table Site Module", + this.progid, + fileSpec, + loc, + type); +}; + +ProtectionTableSiteMod.prototype.getClassObject = function(compMgr, cid, iid) { + if (!cid.equals(this.cid)) + throw Components.results.NS_ERROR_NO_INTERFACE; + if (!iid.equals(Ci.nsIFactory)) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + return this.factory; +} + +ProtectionTableSiteMod.prototype.canUnload = function(compMgr) { + return true; +} + +ProtectionTableSiteMod.prototype.factory = { + createInstance: function(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return (new ProtectionTableSite()).QueryInterface(iid); + } +}; + +var SiteModInst = new ProtectionTableSiteMod(); + +function NSGetModule(compMgr, fileSpec) { + return SiteModInst; +} diff --git a/mozilla/toolkit/components/protection/src/protectionTableUrl.js b/mozilla/toolkit/components/protection/src/protectionTableUrl.js new file mode 100644 index 00000000000..26da9e7ddc9 --- /dev/null +++ b/mozilla/toolkit/components/protection/src/protectionTableUrl.js @@ -0,0 +1,92 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const G_GDEBUG = false; + +// Use subscript loader to load files. The files in ../content get mapped +// to chrome://global/content/protection/. Order matters if one file depends +// on another file during initialization. +const LIB_FILES = [ + "chrome://global/content/protection/js/lang.js", + + "chrome://global/content/protection/moz/preferences.js", + "chrome://global/content/protection/moz/filesystem.js", + "chrome://global/content/protection/moz/debug.js", // req js/lang.js moz/prefs.js moz/filesystem.js + + "chrome://global/content/protection/map.js", + "chrome://global/content/protection/url-canonicalizer.js" +]; + +for (var i = 0, libFile; libFile = LIB_FILES[i]; ++i) { + dump('*** loading subscript ' + libFile + '\n'); + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript(libFile); +} + +function ProtectionTableUrl() { + G_Map.call(this); + this.debugZone = "trtable-url"; +} + +ProtectionTableUrl.inherits(G_Map); + +/** + * Look up a URL in a URL table + * + * @returns Boolean true if the canonicalized url is in the table + */ +ProtectionTableUrl.prototype.find = function(url) { + var canonicalized = PROT_URLCanonicalizer.canonicalizeURL_(url); + // Uncomment for debugging + G_Debug(this, "Looking up: " + url + " (" + canonicalized + ")"); + return this.find_(canonicalized); +} + + +// Module object +function ProtectionTableUrlMod() { + this.firstTime = true; + this.cid = Components.ID("{43399ee0-da0b-46a8-9541-08721265981c}"); + this.progid = "@mozilla.org/protection/protectiontable;1?type=url"; +} + +ProtectionTableUrlMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) { + if (this.firstTime) { + this.firstTime = false; + throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN; + } + compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar); + compMgr.registerFactoryLocation(this.cid, + "Protection Table Url Module", + this.progid, + fileSpec, + loc, + type); +}; + +ProtectionTableUrlMod.prototype.getClassObject = function(compMgr, cid, iid) { + if (!cid.equals(this.cid)) + throw Components.results.NS_ERROR_NO_INTERFACE; + if (!iid.equals(Ci.nsIFactory)) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + return this.factory; +} + +ProtectionTableUrlMod.prototype.canUnload = function(compMgr) { + return true; +} + +ProtectionTableUrlMod.prototype.factory = { + createInstance: function(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return (new ProtectionTableUrl()).QueryInterface(iid); + } +}; + +var UrlModInst = new ProtectionTableUrlMod(); + +function NSGetModule(compMgr, fileSpec) { + return UrlModInst; +} diff --git a/mozilla/toolkit/components/protection/tests/Makefile.in b/mozilla/toolkit/components/protection/tests/Makefile.in new file mode 100644 index 00000000000..6bc0c7f69a9 --- /dev/null +++ b/mozilla/toolkit/components/protection/tests/Makefile.in @@ -0,0 +1,47 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Netscape Communications Corporation. +# Portions created by the Initial Developer are Copyright (C) 1998 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/rules.mk + diff --git a/mozilla/toolkit/components/protection/tests/jar.mn b/mozilla/toolkit/components/protection/tests/jar.mn new file mode 100644 index 00000000000..44c69aa36ae --- /dev/null +++ b/mozilla/toolkit/components/protection/tests/jar.mn @@ -0,0 +1,2 @@ +toolkit.jar: ++ content/global/protection/unittests.xul (unittests.xul) diff --git a/mozilla/toolkit/components/protection/tests/unittests.xul b/mozilla/toolkit/components/protection/tests/unittests.xul new file mode 100644 index 00000000000..6b232305b2b --- /dev/null +++ b/mozilla/toolkit/components/protection/tests/unittests.xul @@ -0,0 +1,178 @@ + + + + +