From 943c808476fc4ccea2175a4f76bc05346c53c209 Mon Sep 17 00:00:00 2001 From: "ericroman@google.com" Date: Mon, 23 Feb 2009 19:10:45 +0000 Subject: Add ProxyResolverV8 class.darin@chromium.org is the original author of 'proxy_resolver_v8.cc' and 'proxy_resolver_script.h'. Review URL: http://codereview.chromium.org/21391 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@10199 0039d316-1c4b-4281-b951-d872f2087c98 --- net/base/net_error_list.h | 5 +- net/data/proxy_resolver_v8_unittest/direct.js | 4 + .../missing_close_brace.js | 6 + .../proxy_resolver_v8_unittest/no_entrypoint.js | 2 + .../pac_library_unittest.js | 130 ++++++++++ net/data/proxy_resolver_v8_unittest/passthrough.js | 45 ++++ .../return_empty_string.js | 4 + .../proxy_resolver_v8_unittest/return_function.js | 4 + .../proxy_resolver_v8_unittest/return_integer.js | 4 + net/data/proxy_resolver_v8_unittest/return_null.js | 4 + .../proxy_resolver_v8_unittest/return_object.js | 4 + .../proxy_resolver_v8_unittest/return_undefined.js | 4 + .../proxy_resolver_v8_unittest/return_unicode.js | 4 + .../proxy_resolver_v8_unittest/side_effects.js | 10 + .../unhandled_exception.js | 5 + net/proxy/proxy_resolver_script.h | 267 +++++++++++++++++++++ net/proxy/proxy_resolver_v8.cc | 131 ++++++++++ net/proxy/proxy_resolver_v8.h | 56 +++++ net/proxy/proxy_resolver_v8_unittest.cc | 232 ++++++++++++++++++ 19 files changed, 920 insertions(+), 1 deletion(-) create mode 100644 net/data/proxy_resolver_v8_unittest/direct.js create mode 100644 net/data/proxy_resolver_v8_unittest/missing_close_brace.js create mode 100644 net/data/proxy_resolver_v8_unittest/no_entrypoint.js create mode 100644 net/data/proxy_resolver_v8_unittest/pac_library_unittest.js create mode 100644 net/data/proxy_resolver_v8_unittest/passthrough.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_empty_string.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_function.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_integer.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_null.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_object.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_undefined.js create mode 100644 net/data/proxy_resolver_v8_unittest/return_unicode.js create mode 100644 net/data/proxy_resolver_v8_unittest/side_effects.js create mode 100644 net/data/proxy_resolver_v8_unittest/unhandled_exception.js create mode 100644 net/proxy/proxy_resolver_script.h create mode 100644 net/proxy/proxy_resolver_v8.cc create mode 100644 net/proxy/proxy_resolver_v8.h create mode 100644 net/proxy/proxy_resolver_v8_unittest.cc diff --git a/net/base/net_error_list.h b/net/base/net_error_list.h index c472f54..c2722bd 100644 --- a/net/base/net_error_list.h +++ b/net/base/net_error_list.h @@ -227,9 +227,12 @@ NET_ERROR(RESPONSE_HEADERS_TOO_BIG, -325) // The PAC requested by HTTP did not have a valid status code (non-200). NET_ERROR(PAC_STATUS_NOT_OK, -326) +// The evaluation of the PAC script failed. +NET_ERROR(PAC_SCRIPT_FAILED, -327) + // The response was 401 (Unauthorized), yet the request was a CONNECT request // to a proxy. -NET_ERROR(UNEXPECTED_SERVER_AUTH, -327) +NET_ERROR(UNEXPECTED_SERVER_AUTH, -328) // The cache does not have the requested entry. NET_ERROR(CACHE_MISS, -400) diff --git a/net/data/proxy_resolver_v8_unittest/direct.js b/net/data/proxy_resolver_v8_unittest/direct.js new file mode 100644 index 0000000..43a04da --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/direct.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return "DIRECT"; +} + diff --git a/net/data/proxy_resolver_v8_unittest/missing_close_brace.js b/net/data/proxy_resolver_v8_unittest/missing_close_brace.js new file mode 100644 index 0000000..8018f8f --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/missing_close_brace.js @@ -0,0 +1,6 @@ +// This PAC script is invalid, because there is a missing close brace +// on the function FindProxyForURL(). + +function FindProxyForURL(url, host) { + return "DIRECT"; + diff --git a/net/data/proxy_resolver_v8_unittest/no_entrypoint.js b/net/data/proxy_resolver_v8_unittest/no_entrypoint.js new file mode 100644 index 0000000..8993059 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/no_entrypoint.js @@ -0,0 +1,2 @@ +var x = "This is an invalid PAC script because it lacks a " + + "FindProxyForURL() function"; diff --git a/net/data/proxy_resolver_v8_unittest/pac_library_unittest.js b/net/data/proxy_resolver_v8_unittest/pac_library_unittest.js new file mode 100644 index 0000000..d460b99 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/pac_library_unittest.js @@ -0,0 +1,130 @@ +// This should output "PROXY success:80" if all the tests pass. +// Otherwise it will output "PROXY failure:". +// +// This aims to unit-test the PAC library functions, which are +// exposed in the PAC's execution environment. (Namely, dnsDomainLevels, +// timeRange, etc.) + +function FindProxyForURL(url, host) { + var numTestsFailed = 0; + + // Run all the tests + for (var test in Tests) { + var t = new TestContext(test); + + // Run the test. + Tests[test](t); + + if (t.failed()) { + numTestsFailed++; + } + } + + if (numTestsFailed == 0) { + return "PROXY success:80"; + } + return "PROXY failure:" + numTestsFailed; +} + +// -------------------------- +// Tests +// -------------------------- + +var Tests = {}; + +Tests.testDnsDomainIs = function(t) { + t.expectTrue(dnsDomainIs("google.com", ".com")); + t.expectTrue(dnsDomainIs("google.co.uk", ".co.uk")); + t.expectFalse(dnsDomainIs("google.com", ".co.uk")); +}; + +Tests.testDnsDomainLevels = function(t) { + t.expectEquals(0, dnsDomainLevels("www")); + t.expectEquals(2, dnsDomainLevels("www.google.com")); + t.expectEquals(3, dnsDomainLevels("192.168.1.1")); +}; + +Tests.testIsInNet = function(t) { + // TODO(eroman): + + // t.expectTrue( + // isInNet("192.89.132.25", "192.89.132.25", "255.255.255.255")); + // t.expectFalse( + // isInNet("193.89.132.25", "192.89.132.25", "255.255.255.255")); + // + // t.expectTrue(isInNet("192.89.132.25", "192.89.0.0", "255.255.0.0")); + // t.expectFalse(isInNet("193.89.132.25", "192.89.0.0", "255.255.0.0")); +}; + +Tests.testIsPlainHostName = function(t) { + t.expectTrue(isPlainHostName("google")); + t.expectFalse(isPlainHostName("google.com")); +}; + +Tests.testLocalHostOrDomainIs = function(t) { + t.expectTrue(localHostOrDomainIs("www.google.com", "www.google.com")); + t.expectTrue(localHostOrDomainIs("www", "www.google.com")); + t.expectFalse(localHostOrDomainIs("maps.google.com", "www.google.com")); +}; + +Tests.testShExpMatch = function(t) { + // TODO(eroman): + + //t.expectTrue(shExpMatch("http://maps.google.com/blah/foo/moreblah.jpg", + // ".*/foo/.*jpg")); + + //t.expectFalse(shExpMatch("http://maps.google.com/blah/foo/moreblah.jpg", + // ".*/foo/.*.html")); +}; + +Tests.testWeekdayRange = function(t) { + // TODO(eroman) +}; + +Tests.testDateRange = function(t) { + // TODO(eroman) +}; + +Tests.testTimeRange = function(t) { + // TODO(eroman) +}; + +// -------------------------- +// Helpers +// -------------------------- + +// |name| is the name of the test being executed, it will be used when logging +// errors. +function TestContext(name) { + this.numFailures_ = 0; + this.name_ = name; +}; + +TestContext.prototype.failed = function() { + return this.numFailures_ != 0; +}; + +TestContext.prototype.expectEquals = function(expectation, actual) { + if (!(expectation === actual)) { + this.numFailures_++; + this.log("FAIL: expected: " + expectation + ", actual: " + actual); + } +}; + +TestContext.prototype.expectTrue = function(x) { + this.expectEquals(true, x); +}; + +TestContext.prototype.expectFalse = function(x) { + this.expectEquals(false, x); +}; + +TestContext.prototype.log = function(x) { + // Prefix with the test name that generated the log. + try { + alert(this.name_ + ": " + x); + } catch(e) { + // In case alert() is not defined. + } +}; + diff --git a/net/data/proxy_resolver_v8_unittest/passthrough.js b/net/data/proxy_resolver_v8_unittest/passthrough.js new file mode 100644 index 0000000..832ac66 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/passthrough.js @@ -0,0 +1,45 @@ +// Return a single-proxy result, which encodes ALL the arguments that were +// passed to FindProxyForURL(). + +function FindProxyForURL(url, host) { + if (arguments.length != 2) { + throw "Wrong number of arguments passed to FindProxyForURL!"; + return "FAIL"; + } + + return "PROXY " + makePseudoHost(url + "." + host); +} + +// Form a string that kind-of resembles a host. We will replace any +// non-alphanumeric character with a dot, then fix up the oddly placed dots. +function makePseudoHost(str) { + var result = ""; + + for (var i = 0; i < str.length; ++i) { + var c = str.charAt(i); + if (!isValidPseudoHostChar(c)) { + c = '.'; // Replace unsupported characters with a dot. + } + + // Take care not to place multiple adjacent dots, + // a dot at the beginning, or a dot at the end. + if (c == '.' && + (result.length == 0 || + i == str.length - 1 || + result.charAt(result.length - 1) == '.')) { + continue; + } + result += c; + } + return result; +} + +function isValidPseudoHostChar(c) { + if (c >= '0' && c <= '9') + return true; + if (c >= 'a' && c <= 'z') + return true; + if (c >= 'A' && c <= 'Z') + return true; + return false; +} diff --git a/net/data/proxy_resolver_v8_unittest/return_empty_string.js b/net/data/proxy_resolver_v8_unittest/return_empty_string.js new file mode 100644 index 0000000..3342196 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_empty_string.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return ""; +} + diff --git a/net/data/proxy_resolver_v8_unittest/return_function.js b/net/data/proxy_resolver_v8_unittest/return_function.js new file mode 100644 index 0000000..9005553 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_function.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return FindProxyForURL; +} + diff --git a/net/data/proxy_resolver_v8_unittest/return_integer.js b/net/data/proxy_resolver_v8_unittest/return_integer.js new file mode 100644 index 0000000..d86b299 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_integer.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return 0; +} + diff --git a/net/data/proxy_resolver_v8_unittest/return_null.js b/net/data/proxy_resolver_v8_unittest/return_null.js new file mode 100644 index 0000000..6cf90c5 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_null.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return null; +} + diff --git a/net/data/proxy_resolver_v8_unittest/return_object.js b/net/data/proxy_resolver_v8_unittest/return_object.js new file mode 100644 index 0000000..3824f8a --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_object.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return {result: "PROXY foo"}; +} + diff --git a/net/data/proxy_resolver_v8_unittest/return_undefined.js b/net/data/proxy_resolver_v8_unittest/return_undefined.js new file mode 100644 index 0000000..0f0aa98 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_undefined.js @@ -0,0 +1,4 @@ +function FindProxyForURL(url, host) { + return undefined; +} + diff --git a/net/data/proxy_resolver_v8_unittest/return_unicode.js b/net/data/proxy_resolver_v8_unittest/return_unicode.js new file mode 100644 index 0000000..5ecdd1c --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/return_unicode.js @@ -0,0 +1,4 @@ +// U+200B is the codepoint for zero-width-space. +function FindProxyForURL(url, host) { + return "PROXY foo.com\u200B"; +} diff --git a/net/data/proxy_resolver_v8_unittest/side_effects.js b/net/data/proxy_resolver_v8_unittest/side_effects.js new file mode 100644 index 0000000..39b3b2d --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/side_effects.js @@ -0,0 +1,10 @@ +if (!gCounter) { + // We write it this way so if the script gets loaded twice, + // gCounter remains dirty. + var gCounter = 0; +} + +function FindProxyForURL(url, host) { + return "PROXY sideffect_" + gCounter++; +} + diff --git a/net/data/proxy_resolver_v8_unittest/unhandled_exception.js b/net/data/proxy_resolver_v8_unittest/unhandled_exception.js new file mode 100644 index 0000000..9cc2856 --- /dev/null +++ b/net/data/proxy_resolver_v8_unittest/unhandled_exception.js @@ -0,0 +1,5 @@ +function FindProxyForURL(url, host) { + // This will throw a runtime exception. + return "PROXY x" + undefined_variable; +} + diff --git a/net/proxy/proxy_resolver_script.h b/net/proxy/proxy_resolver_script.h new file mode 100644 index 0000000..ddcb66b --- /dev/null +++ b/net/proxy/proxy_resolver_script.h @@ -0,0 +1,267 @@ +/* ***** 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): + * Akhil Arora + * Tomi Leppikangas + * Darin Fisher + * + * 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 ***** */ + +#ifndef NET_PROXY_PROXY_RESOLVER_SCRIPT_H_ +#define NET_PROXY_PROXY_RESOLVER_SCRIPT_H_ + +// The following code was formatted from: +// 'mozilla/netwerk/base/src/nsProxyAutoConfig.js' (1.55) +// +// Using the command: +// $ cat nsProxyAutoConfig.js | +// awk '/var pacUtils/,/EOF/' | +// sed -e 's/^\s*$/""/g' | +// sed -e 's/"\s*[+]\s*$/"/g' | +// sed -e 's/"$/" \\/g' | +// grep -v '^var pacUtils =' +#define PROXY_RESOLVER_SCRIPT \ + "function dnsDomainIs(host, domain) {\n" \ + " return (host.length >= domain.length &&\n" \ + " host.substring(host.length - domain.length) == domain);\n" \ + "}\n" \ + "" \ + "function dnsDomainLevels(host) {\n" \ + " return host.split('.').length-1;\n" \ + "}\n" \ + "" \ + "function convert_addr(ipchars) {\n" \ + " var bytes = ipchars.split('.');\n" \ + " var result = ((bytes[0] & 0xff) << 24) |\n" \ + " ((bytes[1] & 0xff) << 16) |\n" \ + " ((bytes[2] & 0xff) << 8) |\n" \ + " (bytes[3] & 0xff);\n" \ + " return result;\n" \ + "}\n" \ + "" \ + "function isInNet(ipaddr, pattern, maskstr) {\n" \ + " var test = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/(ipaddr);\n" \ + " if (test == null) {\n" \ + " ipaddr = dnsResolve(ipaddr);\n" \ + " if (ipaddr == null)\n" \ + " return false;\n" \ + " } else if (test[1] > 255 || test[2] > 255 || \n" \ + " test[3] > 255 || test[4] > 255) {\n" \ + " return false; // not an IP address\n" \ + " }\n" \ + " var host = convert_addr(ipaddr);\n" \ + " var pat = convert_addr(pattern);\n" \ + " var mask = convert_addr(maskstr);\n" \ + " return ((host & mask) == (pat & mask));\n" \ + " \n" \ + "}\n" \ + "" \ + "function isPlainHostName(host) {\n" \ + " return (host.search('\\\\.') == -1);\n" \ + "}\n" \ + "" \ + "function isResolvable(host) {\n" \ + " var ip = dnsResolve(host);\n" \ + " return (ip != null);\n" \ + "}\n" \ + "" \ + "function localHostOrDomainIs(host, hostdom) {\n" \ + " return (host == hostdom) ||\n" \ + " (hostdom.lastIndexOf(host + '.', 0) == 0);\n" \ + "}\n" \ + "" \ + "function shExpMatch(url, pattern) {\n" \ + " pattern = pattern.replace(/\\./g, '\\\\.');\n" \ + " pattern = pattern.replace(/\\*/g, '.*');\n" \ + " pattern = pattern.replace(/\\?/g, '.');\n" \ + " var newRe = new RegExp('^'+pattern+'$');\n" \ + " return newRe.test(url);\n" \ + "}\n" \ + "" \ + "var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};\n" \ + "" \ + "var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};\n" \ + "" \ + "function weekdayRange() {\n" \ + " function getDay(weekday) {\n" \ + " if (weekday in wdays) {\n" \ + " return wdays[weekday];\n" \ + " }\n" \ + " return -1;\n" \ + " }\n" \ + " var date = new Date();\n" \ + " var argc = arguments.length;\n" \ + " var wday;\n" \ + " if (argc < 1)\n" \ + " return false;\n" \ + " if (arguments[argc - 1] == 'GMT') {\n" \ + " argc--;\n" \ + " wday = date.getUTCDay();\n" \ + " } else {\n" \ + " wday = date.getDay();\n" \ + " }\n" \ + " var wd1 = getDay(arguments[0]);\n" \ + " var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;\n" \ + " return (wd1 == -1 || wd2 == -1) ? false\n" \ + " : (wd1 <= wday && wday <= wd2);\n" \ + "}\n" \ + "" \ + "function dateRange() {\n" \ + " function getMonth(name) {\n" \ + " if (name in months) {\n" \ + " return months[name];\n" \ + " }\n" \ + " return -1;\n" \ + " }\n" \ + " var date = new Date();\n" \ + " var argc = arguments.length;\n" \ + " if (argc < 1) {\n" \ + " return false;\n" \ + " }\n" \ + " var isGMT = (arguments[argc - 1] == 'GMT');\n" \ + "\n" \ + " if (isGMT) {\n" \ + " argc--;\n" \ + " }\n" \ + " // function will work even without explict handling of this case\n" \ + " if (argc == 1) {\n" \ + " var tmp = parseInt(arguments[0]);\n" \ + " if (isNaN(tmp)) {\n" \ + " return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==\n" \ + "getMonth(arguments[0]));\n" \ + " } else if (tmp < 32) {\n" \ + " return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp);\n" \ + " } else { \n" \ + " return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) ==\n" \ + "tmp);\n" \ + " }\n" \ + " }\n" \ + " var year = date.getFullYear();\n" \ + " var date1, date2;\n" \ + " date1 = new Date(year, 0, 1, 0, 0, 0);\n" \ + " date2 = new Date(year, 11, 31, 23, 59, 59);\n" \ + " var adjustMonth = false;\n" \ + " for (var i = 0; i < (argc >> 1); i++) {\n" \ + " var tmp = parseInt(arguments[i]);\n" \ + " if (isNaN(tmp)) {\n" \ + " var mon = getMonth(arguments[i]);\n" \ + " date1.setMonth(mon);\n" \ + " } else if (tmp < 32) {\n" \ + " adjustMonth = (argc <= 2);\n" \ + " date1.setDate(tmp);\n" \ + " } else {\n" \ + " date1.setFullYear(tmp);\n" \ + " }\n" \ + " }\n" \ + " for (var i = (argc >> 1); i < argc; i++) {\n" \ + " var tmp = parseInt(arguments[i]);\n" \ + " if (isNaN(tmp)) {\n" \ + " var mon = getMonth(arguments[i]);\n" \ + " date2.setMonth(mon);\n" \ + " } else if (tmp < 32) {\n" \ + " date2.setDate(tmp);\n" \ + " } else {\n" \ + " date2.setFullYear(tmp);\n" \ + " }\n" \ + " }\n" \ + " if (adjustMonth) {\n" \ + " date1.setMonth(date.getMonth());\n" \ + " date2.setMonth(date.getMonth());\n" \ + " }\n" \ + " if (isGMT) {\n" \ + " var tmp = date;\n" \ + " tmp.setFullYear(date.getUTCFullYear());\n" \ + " tmp.setMonth(date.getUTCMonth());\n" \ + " tmp.setDate(date.getUTCDate());\n" \ + " tmp.setHours(date.getUTCHours());\n" \ + " tmp.setMinutes(date.getUTCMinutes());\n" \ + " tmp.setSeconds(date.getUTCSeconds());\n" \ + " date = tmp;\n" \ + " }\n" \ + " return ((date1 <= date) && (date <= date2));\n" \ + "}\n" \ + "" \ + "function timeRange() {\n" \ + " var argc = arguments.length;\n" \ + " var date = new Date();\n" \ + " var isGMT= false;\n" \ + "\n" \ + " if (argc < 1) {\n" \ + " return false;\n" \ + " }\n" \ + " if (arguments[argc - 1] == 'GMT') {\n" \ + " isGMT = true;\n" \ + " argc--;\n" \ + " }\n" \ + "\n" \ + " var hour = isGMT ? date.getUTCHours() : date.getHours();\n" \ + " var date1, date2;\n" \ + " date1 = new Date();\n" \ + " date2 = new Date();\n" \ + "\n" \ + " if (argc == 1) {\n" \ + " return (hour == arguments[0]);\n" \ + " } else if (argc == 2) {\n" \ + " return ((arguments[0] <= hour) && (hour <= arguments[1]));\n" \ + " } else {\n" \ + " switch (argc) {\n" \ + " case 6:\n" \ + " date1.setSeconds(arguments[2]);\n" \ + " date2.setSeconds(arguments[5]);\n" \ + " case 4:\n" \ + " var middle = argc >> 1;\n" \ + " date1.setHours(arguments[0]);\n" \ + " date1.setMinutes(arguments[1]);\n" \ + " date2.setHours(arguments[middle]);\n" \ + " date2.setMinutes(arguments[middle + 1]);\n" \ + " if (middle == 2) {\n" \ + " date2.setSeconds(59);\n" \ + " }\n" \ + " break;\n" \ + " default:\n" \ + " throw 'timeRange: bad number of arguments'\n" \ + " }\n" \ + " }\n" \ + "\n" \ + " if (isGMT) {\n" \ + " date.setFullYear(date.getUTCFullYear());\n" \ + " date.setMonth(date.getUTCMonth());\n" \ + " date.setDate(date.getUTCDate());\n" \ + " date.setHours(date.getUTCHours());\n" \ + " date.setMinutes(date.getUTCMinutes());\n" \ + " date.setSeconds(date.getUTCSeconds());\n" \ + " }\n" \ + " return ((date1 <= date) && (date <= date2));\n" \ + "}\n" \ + +#endif // NET_PROXY_PROXY_RESOLVER_SCRIPT_H_ diff --git a/net/proxy/proxy_resolver_v8.cc b/net/proxy/proxy_resolver_v8.cc new file mode 100644 index 0000000..f26a6f6 --- /dev/null +++ b/net/proxy/proxy_resolver_v8.cc @@ -0,0 +1,131 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this +// source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +#include "net/proxy/proxy_resolver_v8.h" + +#include "net/base/net_errors.h" +#include "net/proxy/proxy_resolver_script.h" +#include "v8/include/v8.h" + +// TODO(eroman): +// - javascript binding for "alert()" +// - javascript binding for "myIpAddress()" +// - javascript binding for "dnsResolve()" +// - log errors for PAC authors (like if "FindProxyForURL" is missing, or +// the javascript was malformed, etc.. + +namespace net { + +namespace { + +std::string V8StringToStdString(v8::Handle s) { + int len = s->Utf8Length(); + std::string result; + s->WriteUtf8(WriteInto(&result, len + 1), len); + return result; +} + +v8::Local StdStringToV8String(const std::string& s) { + return v8::String::New(s.data(), s.size()); +} + +} // namespace + +// ProxyResolverV8::Context --------------------------------------------------- + +class ProxyResolverV8::Context { + public: + explicit Context(const std::string& pac_data) { + InitV8(pac_data); + } + + ~Context() { + v8::Locker locked; + v8::HandleScope scope; + + v8_context_.Dispose(); + } + + int ResolveProxy(const GURL& query_url, ProxyInfo* results) { + v8::Locker locked; + v8::HandleScope scope; + + v8::Context::Scope function_scope(v8_context_); + + v8::Local function = + v8_context_->Global()->Get(v8::String::New("FindProxyForURL")); + if (!function->IsFunction()) + return ERR_PAC_SCRIPT_FAILED; + + v8::Handle argv[] = { + StdStringToV8String(query_url.spec()), + StdStringToV8String(query_url.host()), + }; + + v8::Local ret = v8::Function::Cast(*function)->Call( + v8_context_->Global(), arraysize(argv), argv); + + // The handle is empty if an exception was thrown from "FindProxyForURL". + if (ret.IsEmpty()) + return ERR_PAC_SCRIPT_FAILED; + + if (!ret->IsString()) + return ERR_PAC_SCRIPT_FAILED; + + std::string ret_str = V8StringToStdString(ret->ToString()); + + results->UsePacString(ret_str); + + return OK; + } + + private: + void InitV8(const std::string& pac_data) { + v8::Locker locked; + v8::HandleScope scope; + + v8::Local global_template = v8::ObjectTemplate::New(); + + v8_context_ = v8::Context::New(NULL, global_template); + v8::Context::Scope ctx(v8_context_); + + std::string text_raw = pac_data + PROXY_RESOLVER_SCRIPT; + + v8::Local text = StdStringToV8String(text_raw); + v8::ScriptOrigin origin = v8::ScriptOrigin(v8::String::New("")); + v8::Local code = v8::Script::Compile(text, &origin); + if (!code.IsEmpty()) + code->Run(); + } + + v8::Persistent v8_context_; +}; + +// ProxyResolverV8 ------------------------------------------------------------ + +// the |false| argument to ProxyResolver means the ProxyService will handle +// downloading of the PAC script, and notify changes through SetPacScript(). +ProxyResolverV8::ProxyResolverV8() : ProxyResolver(false /*does_fetch*/) {} + +ProxyResolverV8::~ProxyResolverV8() {} + +int ProxyResolverV8::GetProxyForURL(const GURL& query_url, + const GURL& /*pac_url*/, + ProxyInfo* results) { + // If the V8 instance has not been initialized (either because SetPacScript() + // wasn't called yet, or because it was called with empty string). + if (!context_.get()) + return ERR_FAILED; + + // Otherwise call into V8. + return context_->ResolveProxy(query_url, results); +} + +void ProxyResolverV8::SetPacScript(const std::string& data) { + context_.reset(); + if (!data.empty()) + context_.reset(new Context(data)); +} + +} // namespace net diff --git a/net/proxy/proxy_resolver_v8.h b/net/proxy/proxy_resolver_v8.h new file mode 100644 index 0000000..312b557 --- /dev/null +++ b/net/proxy/proxy_resolver_v8.h @@ -0,0 +1,56 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_PROXY_PROXY_RESOLVER_V8_H_ +#define NET_PROXY_PROXY_RESOLVER_V8_H_ + +#include + +#include "base/scoped_ptr.h" +#include "net/proxy/proxy_service.h" + +namespace net { + +// Implementation of ProxyResolver that uses V8 to evaluate PAC scripts. +// +// ---------------------------------------------------------------------------- +// !!! Important note on threading model: +// ---------------------------------------------------------------------------- +// There can be only one instance of V8 running at a time. To enforce this +// constraint, ProxyResolverV8 holds a v8::Locker during execution. Therefore +// it is OK to run multiple instances of ProxyResolverV8 on different threads, +// since only one will be running inside V8 at a time. +// +// It is important that *ALL* instances of V8 in the process be using +// v8::Locker. If not there can be race conditions beween the non-locked V8 +// instances and the locked V8 instances used by ProxyResolverV8 (assuming they +// run on different threads). +// +// This is the case with the V8 instance used by chromium's renderer -- it runs +// on a different thread from ProxyResolver (renderer thread vs PAC thread), +// and does not use locking since it expects to be alone. +class ProxyResolverV8 : public ProxyResolver { + public: + ProxyResolverV8(); + ~ProxyResolverV8(); + + // ProxyResolver implementation: + virtual int GetProxyForURL(const GURL& query_url, + const GURL& /*pac_url*/, + ProxyInfo* results); + virtual void SetPacScript(const std::string& bytes); + + private: + // Context holds the Javascript state for the most recently loaded PAC + // script. It corresponds with the data from the last call to + // SetPacScript(). + class Context; + scoped_ptr context_; + + DISALLOW_COPY_AND_ASSIGN(ProxyResolverV8); +}; + +} // namespace net + +#endif // NET_PROXY_PROXY_RESOLVER_V8_H_ diff --git a/net/proxy/proxy_resolver_v8_unittest.cc b/net/proxy/proxy_resolver_v8_unittest.cc new file mode 100644 index 0000000..b576aa9 --- /dev/null +++ b/net/proxy/proxy_resolver_v8_unittest.cc @@ -0,0 +1,232 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/file_util.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "net/base/net_errors.h" +#include "net/base/net_util.h" +#include "net/proxy/proxy_resolver_v8.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Initialize |resolver| with the PAC script data at |filename|. +void InitWithScriptFromDisk(net::ProxyResolver* resolver, + const char* filename) { + FilePath path; + PathService::Get(base::DIR_SOURCE_ROOT, &path); + path = path.AppendASCII("net"); + path = path.AppendASCII("data"); + path = path.AppendASCII("proxy_resolver_v8_unittest"); + path = path.AppendASCII(filename); + + // Try to read the file from disk. + std::string file_contents; + bool ok = file_util::ReadFileToString(path, &file_contents); + + // If we can't load the file from disk, something is misconfigured. + LOG_IF(ERROR, !ok) << "Failed to read file: " << path.value(); + ASSERT_TRUE(ok); + + // Load the PAC script into the ProxyResolver. + resolver->SetPacScript(file_contents); +} + +// Doesn't really matter what these values are for many of the tests. +const GURL kQueryUrl("http://www.google.com"); +const GURL kPacUrl; + +} + +TEST(ProxyResolverV8Test, Direct) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "direct.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + EXPECT_EQ(net::OK, result); + EXPECT_TRUE(proxy_info.is_direct()); +} + +TEST(ProxyResolverV8Test, ReturnEmptyString) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "return_empty_string.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + EXPECT_EQ(net::OK, result); + EXPECT_TRUE(proxy_info.is_direct()); +} + +TEST(ProxyResolverV8Test, Basic) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "passthrough.js"); + + // The "FindProxyForURL" of this PAC script simply concatenates all of the + // arguments into a pseudo-host. The purpose of this test is to verify that + // the correct arguments are being passed to FindProxyForURL(). + { + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(GURL("http://query.com/path"), + kPacUrl, &proxy_info); + EXPECT_EQ(net::OK, result); + EXPECT_EQ("http.query.com.path.query.com:80", + proxy_info.proxy_server().ToURI()); + } + { + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(GURL("ftp://query.com:90/path"), + kPacUrl, &proxy_info); + EXPECT_EQ(net::OK, result); + // Note that FindProxyForURL(url, host) does not expect |host| to contain + // the port number. + EXPECT_EQ("ftp.query.com.90.path.query.com:80", + proxy_info.proxy_server().ToURI()); + } +} + +TEST(ProxyResolverV8Test, BadReturnType) { + // These are the filenames of PAC scripts which each return a non-string + // types for FindProxyForURL(). They should all fail with + // ERR_PAC_SCRIPT_FAILED. + static const char* const filenames[] = { + "return_undefined.js", + "return_integer.js", + "return_function.js", + "return_object.js", + // TODO(eroman): Should 'null' be considered equivalent to "DIRECT" ? + "return_null.js" + }; + + for (size_t i = 0; i < arraysize(filenames); ++i) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, filenames[i]); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + EXPECT_EQ(net::ERR_PAC_SCRIPT_FAILED, result); + } +} + +// Try using a PAC script which defines no "FindProxyForURL" function. +TEST(ProxyResolverV8Test, NoEntryPoint) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "no_entrypoint.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + EXPECT_EQ(net::ERR_PAC_SCRIPT_FAILED, result); +} + +// Try loading a malformed PAC script. +TEST(ProxyResolverV8Test, ParseError) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "missing_close_brace.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + EXPECT_EQ(net::ERR_PAC_SCRIPT_FAILED, result); +} + +// Run a PAC script several times, which has side-effects. +TEST(ProxyResolverV8Test, SideEffects) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "side_effects.js"); + + // The PAC script increments a counter each time we invoke it. + for (int i = 0; i < 3; ++i) { + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + EXPECT_EQ(net::OK, result); + EXPECT_EQ(StringPrintf("sideffect_%d:80", i), + proxy_info.proxy_server().ToURI()); + } + + // Reload the script -- the javascript environment should be reset, hence + // the counter starts over. + InitWithScriptFromDisk(&resolver, "side_effects.js"); + + for (int i = 0; i < 3; ++i) { + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + EXPECT_EQ(net::OK, result); + EXPECT_EQ(StringPrintf("sideffect_%d:80", i), + proxy_info.proxy_server().ToURI()); + } +} + +// Execute a PAC script which throws an exception in FindProxyForURL. +TEST(ProxyResolverV8Test, UnhandledException) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "unhandled_exception.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + EXPECT_EQ(net::ERR_PAC_SCRIPT_FAILED, result); +} + +// TODO(eroman): This test is disabed right now, since the parsing of +// host/port doesn't check for non-ascii characters. +TEST(ProxyResolverV8Test, DISABLED_ReturnUnicode) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "return_unicode.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + // The result from this resolve was unparseable, because it + // wasn't ascii. + EXPECT_EQ(net::ERR_PAC_SCRIPT_FAILED, result); +} + +// Test the PAC library functions that we expose in the JS environmnet. +TEST(ProxyResolverV8Test, JavascriptLibrary) { + net::ProxyResolverV8 resolver; + InitWithScriptFromDisk(&resolver, "pac_library_unittest.js"); + + net::ProxyInfo proxy_info; + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + + // If the javascript side of this unit-test fails, it will throw a javascript + // exception. Otherwise it will return "PROXY success:80". + EXPECT_EQ(net::OK, result); + EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI()); +} + +// Try resolving when SetPacScript() has not been called. +TEST(ProxyResolverV8Test, NoSetPacScript) { + net::ProxyResolverV8 resolver; + + net::ProxyInfo proxy_info; + + // Resolve should fail, as we are not yet initialized with a script. + int result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + EXPECT_EQ(net::ERR_FAILED, result); + + // Initialize it. + InitWithScriptFromDisk(&resolver, "direct.js"); + + // Resolve should now succeed. + result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + EXPECT_EQ(net::OK, result); + + // Clear it, by initializing with an empty string. + resolver.SetPacScript(std::string()); + + // Resolve should fail again now. + result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + EXPECT_EQ(net::ERR_FAILED, result); + + // Load a good script once more. + InitWithScriptFromDisk(&resolver, "direct.js"); + result = resolver.GetProxyForURL(kQueryUrl, kPacUrl, &proxy_info); + EXPECT_EQ(net::OK, result); +} \ No newline at end of file -- cgit v1.1