1 // Copyright 2012 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "services/proxy_resolver/proxy_resolver_v8.h"
7 #include "base/compiler_specific.h"
8 #include "base/files/file_util.h"
9 #include "base/path_service.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/stringprintf.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/test/task_environment.h"
14 #include "net/base/net_errors.h"
15 #include "net/base/proxy_string_util.h"
16 #include "net/proxy_resolution/pac_file_data.h"
17 #include "net/proxy_resolution/proxy_info.h"
18 #include "net/test/gtest_util.h"
19 #include "testing/gmock/include/gmock/gmock.h"
20 #include "testing/gtest/include/gtest/gtest.h"
23 using net::test::IsError;
24 using net::test::IsOk;
25 using ::testing::IsEmpty;
27 namespace proxy_resolver {
30 // Javascript bindings for ProxyResolverV8, which returns mock values.
31 // Each time one of the bindings is called into, we push the input into a
32 // list, for later verification.
33 class MockJSBindings : public ProxyResolverV8::JSBindings {
36 : my_ip_address_count(0),
37 my_ip_address_ex_count(0),
38 should_terminate(false) {}
40 void Alert(const std::u16string& message) override {
41 VLOG(1) << "PAC-alert: " << message; // Helpful when debugging.
42 alerts.push_back(base::UTF16ToUTF8(message));
45 bool ResolveDns(const std::string& host,
46 net::ProxyResolveDnsOperation op,
48 bool* terminate) override {
49 *terminate = should_terminate;
51 if (op == net::ProxyResolveDnsOperation::MY_IP_ADDRESS) {
52 my_ip_address_count++;
53 *output = my_ip_address_result;
54 return !my_ip_address_result.empty();
57 if (op == net::ProxyResolveDnsOperation::MY_IP_ADDRESS_EX) {
58 my_ip_address_ex_count++;
59 *output = my_ip_address_ex_result;
60 return !my_ip_address_ex_result.empty();
63 if (op == net::ProxyResolveDnsOperation::DNS_RESOLVE) {
64 dns_resolves.push_back(host);
65 *output = dns_resolve_result;
66 return !dns_resolve_result.empty();
69 if (op == net::ProxyResolveDnsOperation::DNS_RESOLVE_EX) {
70 dns_resolves_ex.push_back(host);
71 *output = dns_resolve_ex_result;
72 return !dns_resolve_ex_result.empty();
79 void OnError(int line_number, const std::u16string& message) override {
80 // Helpful when debugging.
81 VLOG(1) << "PAC-error: [" << line_number << "] " << message;
83 errors.push_back(base::UTF16ToUTF8(message));
84 errors_line_number.push_back(line_number);
87 // Mock values to return.
88 std::string my_ip_address_result;
89 std::string my_ip_address_ex_result;
90 std::string dns_resolve_result;
91 std::string dns_resolve_ex_result;
93 // Inputs we got called with.
94 std::vector<std::string> alerts;
95 std::vector<std::string> errors;
96 std::vector<int> errors_line_number;
97 std::vector<std::string> dns_resolves;
98 std::vector<std::string> dns_resolves_ex;
99 int my_ip_address_count;
100 int my_ip_address_ex_count;
102 // Whether ResolveDns() should terminate script execution.
103 bool should_terminate;
106 class ProxyResolverV8Test : public testing::Test {
108 // Creates a ProxyResolverV8 using the PAC script contained in |filename|. If
109 // called more than once, the previous ProxyResolverV8 is deleted.
110 int CreateResolver(const char* filename) {
112 base::PathService::Get(base::DIR_SOURCE_ROOT, &path);
113 path = path.AppendASCII("services");
114 path = path.AppendASCII("proxy_resolver");
115 path = path.AppendASCII("test");
116 path = path.AppendASCII("data");
117 path = path.AppendASCII("proxy_resolver_v8_unittest");
118 path = path.AppendASCII(filename);
120 // Try to read the file from disk.
121 std::string file_contents;
122 bool ok = base::ReadFileToString(path, &file_contents);
124 // If we can't load the file from disk, something is misconfigured.
126 LOG(ERROR) << "Failed to read file: " << path.value();
127 return net::ERR_FAILED;
130 // Create the ProxyResolver using the PAC script.
131 return ProxyResolverV8::Create(net::PacFileData::FromUTF8(file_contents),
132 bindings(), &resolver_);
135 ProxyResolverV8& resolver() {
140 MockJSBindings* bindings() { return &js_bindings_; }
143 base::test::TaskEnvironment task_environment_;
144 MockJSBindings js_bindings_;
145 std::unique_ptr<ProxyResolverV8> resolver_;
148 TEST_F(ProxyResolverV8Test, Direct) {
149 ASSERT_THAT(CreateResolver("direct.js"), IsOk());
151 net::ProxyInfo proxy_info;
152 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
153 &proxy_info, bindings());
155 EXPECT_THAT(result, IsOk());
156 EXPECT_TRUE(proxy_info.is_direct());
158 EXPECT_EQ(0U, bindings()->alerts.size());
159 EXPECT_EQ(0U, bindings()->errors.size());
162 TEST_F(ProxyResolverV8Test, ReturnEmptyString) {
163 ASSERT_THAT(CreateResolver("return_empty_string.js"), IsOk());
165 net::ProxyInfo proxy_info;
166 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
167 &proxy_info, bindings());
169 EXPECT_THAT(result, IsOk());
170 EXPECT_TRUE(proxy_info.is_direct());
172 EXPECT_EQ(0U, bindings()->alerts.size());
173 EXPECT_EQ(0U, bindings()->errors.size());
176 TEST_F(ProxyResolverV8Test, Basic) {
177 ASSERT_THAT(CreateResolver("passthrough.js"), IsOk());
179 // The "FindProxyForURL" of this PAC script simply concatenates all of the
180 // arguments into a pseudo-host. The purpose of this test is to verify that
181 // the correct arguments are being passed to FindProxyForURL().
183 net::ProxyInfo proxy_info;
184 int result = resolver().GetProxyForURL(GURL("http://query.com/path"),
185 &proxy_info, bindings());
186 EXPECT_THAT(result, IsOk());
187 EXPECT_EQ("http.query.com.path.query.com:80",
188 ProxyServerToProxyUri(proxy_info.proxy_server()));
191 net::ProxyInfo proxy_info;
192 int result = resolver().GetProxyForURL(GURL("ftp://query.com:90/path"),
193 &proxy_info, bindings());
194 EXPECT_THAT(result, IsOk());
195 // Note that FindProxyForURL(url, host) does not expect |host| to contain
197 EXPECT_EQ("ftp.query.com.90.path.query.com:80",
198 ProxyServerToProxyUri(proxy_info.proxy_server()));
200 EXPECT_EQ(0U, bindings()->alerts.size());
201 EXPECT_EQ(0U, bindings()->errors.size());
205 TEST_F(ProxyResolverV8Test, BadReturnType) {
206 // These are the filenames of PAC scripts which each return a non-string
207 // types for FindProxyForURL(). They should all fail with
208 // net::ERR_PAC_SCRIPT_FAILED.
209 static const char* const filenames[] = {
210 "return_undefined.js", "return_integer.js", "return_function.js",
212 // TODO(eroman): Should 'null' be considered equivalent to "DIRECT" ?
215 for (size_t i = 0; i < std::size(filenames); ++i) {
216 ASSERT_THAT(CreateResolver(filenames[i]), IsOk());
218 MockJSBindings bindings;
219 net::ProxyInfo proxy_info;
220 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
221 &proxy_info, &bindings);
223 EXPECT_THAT(result, IsError(net::ERR_PAC_SCRIPT_FAILED));
225 EXPECT_EQ(0U, bindings.alerts.size());
226 ASSERT_EQ(1U, bindings.errors.size());
227 EXPECT_EQ("FindProxyForURL() did not return a string.", bindings.errors[0]);
228 EXPECT_EQ(-1, bindings.errors_line_number[0]);
232 // Try using a PAC script which defines no "FindProxyForURL" function.
233 TEST_F(ProxyResolverV8Test, NoEntryPoint) {
234 EXPECT_THAT(CreateResolver("no_entrypoint.js"),
235 IsError(net::ERR_PAC_SCRIPT_FAILED));
237 ASSERT_EQ(1U, bindings()->errors.size());
238 EXPECT_EQ("FindProxyForURL is undefined or not a function.",
239 bindings()->errors[0]);
240 EXPECT_EQ(-1, bindings()->errors_line_number[0]);
243 // Try loading a malformed PAC script.
244 TEST_F(ProxyResolverV8Test, ParseError) {
245 EXPECT_THAT(CreateResolver("missing_close_brace.js"),
246 IsError(net::ERR_PAC_SCRIPT_FAILED));
248 EXPECT_EQ(0U, bindings()->alerts.size());
250 // We get one error during compilation.
251 ASSERT_EQ(1U, bindings()->errors.size());
253 EXPECT_EQ("Uncaught SyntaxError: Unexpected end of input",
254 bindings()->errors[0]);
255 EXPECT_EQ(7, bindings()->errors_line_number[0]);
258 // Run a PAC script several times, which has side-effects.
259 TEST_F(ProxyResolverV8Test, SideEffects) {
260 ASSERT_THAT(CreateResolver("side_effects.js"), IsOk());
262 // The PAC script increments a counter each time we invoke it.
263 for (int i = 0; i < 3; ++i) {
264 net::ProxyInfo proxy_info;
265 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
266 &proxy_info, bindings());
267 EXPECT_THAT(result, IsOk());
268 EXPECT_EQ(base::StringPrintf("sideffect_%d:80", i),
269 ProxyServerToProxyUri(proxy_info.proxy_server()));
272 // Reload the script -- the javascript environment should be reset, hence
273 // the counter starts over.
274 ASSERT_THAT(CreateResolver("side_effects.js"), IsOk());
276 for (int i = 0; i < 3; ++i) {
277 net::ProxyInfo proxy_info;
278 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
279 &proxy_info, bindings());
280 EXPECT_THAT(result, IsOk());
281 EXPECT_EQ(base::StringPrintf("sideffect_%d:80", i),
282 ProxyServerToProxyUri(proxy_info.proxy_server()));
286 // Execute a PAC script which throws an exception in FindProxyForURL.
287 TEST_F(ProxyResolverV8Test, UnhandledException) {
288 ASSERT_THAT(CreateResolver("unhandled_exception.js"), IsOk());
290 net::ProxyInfo proxy_info;
291 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
292 &proxy_info, bindings());
294 EXPECT_THAT(result, IsError(net::ERR_PAC_SCRIPT_FAILED));
296 EXPECT_EQ(0U, bindings()->alerts.size());
297 ASSERT_EQ(1U, bindings()->errors.size());
298 EXPECT_EQ("Uncaught ReferenceError: undefined_variable is not defined",
299 bindings()->errors[0]);
300 EXPECT_EQ(3, bindings()->errors_line_number[0]);
303 // Execute a PAC script which throws an exception when first accessing
305 TEST_F(ProxyResolverV8Test, ExceptionAccessingFindProxyForURLDuringInit) {
306 EXPECT_EQ(net::ERR_PAC_SCRIPT_FAILED,
307 CreateResolver("exception_findproxyforurl_during_init.js"));
309 ASSERT_EQ(2U, bindings()->errors.size());
310 EXPECT_EQ("Uncaught crash!", bindings()->errors[0]);
311 EXPECT_EQ(9, bindings()->errors_line_number[0]);
312 EXPECT_EQ("Accessing FindProxyForURL threw an exception.",
313 bindings()->errors[1]);
314 EXPECT_EQ(-1, bindings()->errors_line_number[1]);
317 // Execute a PAC script which throws an exception during the second access to
319 TEST_F(ProxyResolverV8Test, ExceptionAccessingFindProxyForURLDuringResolve) {
320 ASSERT_THAT(CreateResolver("exception_findproxyforurl_during_resolve.js"),
323 net::ProxyInfo proxy_info;
324 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
325 &proxy_info, bindings());
327 EXPECT_THAT(result, IsError(net::ERR_PAC_SCRIPT_FAILED));
329 ASSERT_EQ(2U, bindings()->errors.size());
330 EXPECT_EQ("Uncaught crash!", bindings()->errors[0]);
331 EXPECT_EQ(17, bindings()->errors_line_number[0]);
332 EXPECT_EQ("Accessing FindProxyForURL threw an exception.",
333 bindings()->errors[1]);
334 EXPECT_EQ(-1, bindings()->errors_line_number[1]);
337 TEST_F(ProxyResolverV8Test, ReturnUnicode) {
338 ASSERT_THAT(CreateResolver("return_unicode.js"), IsOk());
340 net::ProxyInfo proxy_info;
341 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
342 &proxy_info, bindings());
344 // The result from this resolve was unparseable, because it
346 EXPECT_THAT(result, IsError(net::ERR_PAC_SCRIPT_FAILED));
349 // Test the PAC library functions that we expose in the JS environment.
350 TEST_F(ProxyResolverV8Test, JavascriptLibrary) {
351 ASSERT_THAT(CreateResolver("pac_library_unittest.js"), IsOk());
353 net::ProxyInfo proxy_info;
354 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
355 &proxy_info, bindings());
357 // If the javascript side of this unit-test fails, it will throw a javascript
358 // exception. Otherwise it will return "PROXY success:80".
359 EXPECT_THAT(bindings()->alerts, IsEmpty());
360 EXPECT_THAT(bindings()->errors, IsEmpty());
362 ASSERT_THAT(result, IsOk());
363 EXPECT_EQ("success:80", ProxyServerToProxyUri(proxy_info.proxy_server()));
366 // Test marshalling/un-marshalling of values between C++/V8.
367 TEST_F(ProxyResolverV8Test, V8Bindings) {
368 ASSERT_THAT(CreateResolver("bindings.js"), IsOk());
369 bindings()->dns_resolve_result = "127.0.0.1";
371 net::ProxyInfo proxy_info;
372 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
373 &proxy_info, bindings());
375 EXPECT_THAT(result, IsOk());
376 EXPECT_TRUE(proxy_info.is_direct());
378 EXPECT_EQ(0U, bindings()->errors.size());
380 // Alert was called 5 times.
381 ASSERT_EQ(5U, bindings()->alerts.size());
382 EXPECT_EQ("undefined", bindings()->alerts[0]);
383 EXPECT_EQ("null", bindings()->alerts[1]);
384 EXPECT_EQ("undefined", bindings()->alerts[2]);
385 EXPECT_EQ("[object Object]", bindings()->alerts[3]);
386 EXPECT_EQ("exception from calling toString()", bindings()->alerts[4]);
388 // DnsResolve was called 8 times, however only 2 of those were string
389 // parameters. (so 6 of them failed immediately).
390 ASSERT_EQ(2U, bindings()->dns_resolves.size());
391 EXPECT_EQ("", bindings()->dns_resolves[0]);
392 EXPECT_EQ("arg1", bindings()->dns_resolves[1]);
394 // MyIpAddress was called two times.
395 EXPECT_EQ(2, bindings()->my_ip_address_count);
397 // MyIpAddressEx was called once.
398 EXPECT_EQ(1, bindings()->my_ip_address_ex_count);
400 // DnsResolveEx was called 2 times.
401 ASSERT_EQ(2U, bindings()->dns_resolves_ex.size());
402 EXPECT_EQ("is_resolvable", bindings()->dns_resolves_ex[0]);
403 EXPECT_EQ("foobar", bindings()->dns_resolves_ex[1]);
406 // Test calling a binding (myIpAddress()) from the script's global scope.
407 // http://crbug.com/40026
408 TEST_F(ProxyResolverV8Test, BindingCalledDuringInitialization) {
409 ASSERT_THAT(CreateResolver("binding_from_global.js"), IsOk());
411 // myIpAddress() got called during initialization of the script.
412 EXPECT_EQ(1, bindings()->my_ip_address_count);
414 net::ProxyInfo proxy_info;
415 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
416 &proxy_info, bindings());
418 EXPECT_THAT(result, IsOk());
419 EXPECT_FALSE(proxy_info.is_direct());
420 EXPECT_EQ("127.0.0.1:80", ProxyServerToProxyUri(proxy_info.proxy_server()));
422 // Check that no other bindings were called.
423 EXPECT_EQ(0U, bindings()->errors.size());
424 ASSERT_EQ(0U, bindings()->alerts.size());
425 ASSERT_EQ(0U, bindings()->dns_resolves.size());
426 EXPECT_EQ(0, bindings()->my_ip_address_ex_count);
427 ASSERT_EQ(0U, bindings()->dns_resolves_ex.size());
430 // Try loading a PAC script that ends with a comment and has no terminal
431 // newline. This should not cause problems with the PAC utility functions
432 // that we add to the script's environment.
433 // http://crbug.com/22864
434 TEST_F(ProxyResolverV8Test, EndsWithCommentNoNewline) {
435 ASSERT_THAT(CreateResolver("ends_with_comment.js"), IsOk());
437 net::ProxyInfo proxy_info;
438 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
439 &proxy_info, bindings());
441 EXPECT_THAT(result, IsOk());
442 EXPECT_FALSE(proxy_info.is_direct());
443 EXPECT_EQ("success:80", ProxyServerToProxyUri(proxy_info.proxy_server()));
446 // Try loading a PAC script that ends with a statement and has no terminal
447 // newline. This should not cause problems with the PAC utility functions
448 // that we add to the script's environment.
449 // http://crbug.com/22864
450 TEST_F(ProxyResolverV8Test, EndsWithStatementNoNewline) {
451 ASSERT_THAT(CreateResolver("ends_with_statement_no_semicolon.js"), IsOk());
453 net::ProxyInfo proxy_info;
454 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
455 &proxy_info, bindings());
457 EXPECT_THAT(result, IsOk());
458 EXPECT_FALSE(proxy_info.is_direct());
459 EXPECT_EQ("success:3", ProxyServerToProxyUri(proxy_info.proxy_server()));
462 // Test the return values from myIpAddress(), myIpAddressEx(), dnsResolve(),
463 // dnsResolveEx(), isResolvable(), isResolvableEx(), when the the binding
464 // returns empty string (failure). This simulates the return values from
465 // those functions when the underlying DNS resolution fails.
466 TEST_F(ProxyResolverV8Test, DNSResolutionFailure) {
467 ASSERT_THAT(CreateResolver("dns_fail.js"), IsOk());
469 net::ProxyInfo proxy_info;
470 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
471 &proxy_info, bindings());
473 EXPECT_THAT(result, IsOk());
474 EXPECT_FALSE(proxy_info.is_direct());
475 EXPECT_EQ("success:80", ProxyServerToProxyUri(proxy_info.proxy_server()));
478 TEST_F(ProxyResolverV8Test, DNSResolutionOfInternationDomainName) {
479 ASSERT_THAT(CreateResolver("international_domain_names.js"), IsOk());
481 // Execute FindProxyForURL().
482 net::ProxyInfo proxy_info;
483 int result = resolver().GetProxyForURL(GURL("http://www.google.com"),
484 &proxy_info, bindings());
486 EXPECT_THAT(result, IsOk());
487 EXPECT_TRUE(proxy_info.is_direct());
489 // Check that the international domain name was converted to punycode
490 // before passing it onto the bindings layer.
491 ASSERT_EQ(1u, bindings()->dns_resolves.size());
492 EXPECT_EQ("xn--bcher-kva.ch", bindings()->dns_resolves[0]);
494 ASSERT_EQ(1u, bindings()->dns_resolves_ex.size());
495 EXPECT_EQ("xn--bcher-kva.ch", bindings()->dns_resolves_ex[0]);
498 // Test that when resolving a URL which contains an IPv6 string literal, the
499 // brackets are removed from the host before passing it down to the PAC script.
500 // If we don't do this, then subsequent calls to dnsResolveEx(host) will be
501 // doomed to fail since it won't correspond with a valid name.
502 TEST_F(ProxyResolverV8Test, IPv6HostnamesNotBracketed) {
503 ASSERT_THAT(CreateResolver("resolve_host.js"), IsOk());
505 net::ProxyInfo proxy_info;
506 int result = resolver().GetProxyForURL(
507 GURL("http://[abcd::efff]:99/watsupdawg"), &proxy_info, bindings());
509 EXPECT_THAT(result, IsOk());
510 EXPECT_TRUE(proxy_info.is_direct());
512 // We called dnsResolveEx() exactly once, by passing through the "host"
513 // argument to FindProxyForURL(). The brackets should have been stripped.
514 ASSERT_EQ(1U, bindings()->dns_resolves_ex.size());
515 EXPECT_EQ("abcd::efff", bindings()->dns_resolves_ex[0]);
518 // Test that terminating a script within DnsResolve() leads to eventual
519 // termination of the script. Also test that repeatedly calling terminate is
520 // safe, and running the script again after termination still works.
521 TEST_F(ProxyResolverV8Test, Terminate) {
522 ASSERT_THAT(CreateResolver("terminate.js"), IsOk());
524 // Terminate script execution upon reaching dnsResolve(). Note that
525 // termination may not take effect right away (so the subsequent dnsResolve()
526 // and alert() may be run).
527 bindings()->should_terminate = true;
529 net::ProxyInfo proxy_info;
531 resolver().GetProxyForURL(GURL("http://hang/"), &proxy_info, bindings());
533 // The script execution was terminated.
534 EXPECT_THAT(result, IsError(net::ERR_PAC_SCRIPT_FAILED));
536 EXPECT_EQ(1U, bindings()->dns_resolves.size());
537 EXPECT_GE(2U, bindings()->dns_resolves_ex.size());
538 EXPECT_GE(1U, bindings()->alerts.size());
540 EXPECT_EQ(1U, bindings()->errors.size());
542 // Termination shows up as an uncaught exception without any message.
543 EXPECT_EQ("", bindings()->errors[0]);
545 bindings()->errors.clear();
547 // Try running the script again, this time with a different input which won't
548 // cause a termination+hang.
549 result = resolver().GetProxyForURL(GURL("http://kittens/"), &proxy_info,
552 EXPECT_THAT(result, IsOk());
553 EXPECT_EQ(0u, bindings()->errors.size());
554 EXPECT_EQ("kittens:88", ProxyServerToProxyUri(proxy_info.proxy_server()));
558 } // namespace proxy_resolver