1// Copyright (c) 2010 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "base/file_util.h"
6#include "base/path_service.h"
7#include "base/string_util.h"
8#include "base/stringprintf.h"
9#include "base/utf_string_conversions.h"
10#include "googleurl/src/gurl.h"
11#include "net/base/net_errors.h"
12#include "net/base/net_log_unittest.h"
13#include "net/proxy/proxy_info.h"
14#include "net/proxy/proxy_resolver_js_bindings.h"
15#include "net/proxy/proxy_resolver_v8.h"
16#include "testing/gtest/include/gtest/gtest.h"
17
18namespace net {
19namespace {
20
21// Javascript bindings for ProxyResolverV8, which returns mock values.
22// Each time one of the bindings is called into, we push the input into a
23// list, for later verification.
24class MockJSBindings : public ProxyResolverJSBindings {
25 public:
26  MockJSBindings() : my_ip_address_count(0), my_ip_address_ex_count(0) {}
27
28  virtual void Alert(const string16& message) {
29    VLOG(1) << "PAC-alert: " << message;  // Helpful when debugging.
30    alerts.push_back(UTF16ToUTF8(message));
31  }
32
33  virtual bool MyIpAddress(std::string* ip_address) {
34    my_ip_address_count++;
35    *ip_address = my_ip_address_result;
36    return !my_ip_address_result.empty();
37  }
38
39  virtual bool MyIpAddressEx(std::string* ip_address_list) {
40    my_ip_address_ex_count++;
41    *ip_address_list = my_ip_address_ex_result;
42    return !my_ip_address_ex_result.empty();
43  }
44
45  virtual bool DnsResolve(const std::string& host, std::string* ip_address) {
46    dns_resolves.push_back(host);
47    *ip_address = dns_resolve_result;
48    return !dns_resolve_result.empty();
49  }
50
51  virtual bool DnsResolveEx(const std::string& host,
52                            std::string* ip_address_list) {
53    dns_resolves_ex.push_back(host);
54    *ip_address_list = dns_resolve_ex_result;
55    return !dns_resolve_ex_result.empty();
56  }
57
58  virtual void OnError(int line_number, const string16& message) {
59    // Helpful when debugging.
60    VLOG(1) << "PAC-error: [" << line_number << "] " << message;
61
62    errors.push_back(UTF16ToUTF8(message));
63    errors_line_number.push_back(line_number);
64  }
65
66  virtual void Shutdown() {}
67
68  // Mock values to return.
69  std::string my_ip_address_result;
70  std::string my_ip_address_ex_result;
71  std::string dns_resolve_result;
72  std::string dns_resolve_ex_result;
73
74  // Inputs we got called with.
75  std::vector<std::string> alerts;
76  std::vector<std::string> errors;
77  std::vector<int> errors_line_number;
78  std::vector<std::string> dns_resolves;
79  std::vector<std::string> dns_resolves_ex;
80  int my_ip_address_count;
81  int my_ip_address_ex_count;
82};
83
84// This is the same as ProxyResolverV8, but it uses mock bindings in place of
85// the default bindings, and has a helper function to load PAC scripts from
86// disk.
87class ProxyResolverV8WithMockBindings : public ProxyResolverV8 {
88 public:
89  ProxyResolverV8WithMockBindings() : ProxyResolverV8(new MockJSBindings()) {}
90
91  MockJSBindings* mock_js_bindings() const {
92    return reinterpret_cast<MockJSBindings*>(js_bindings());
93  }
94
95  // Initialize with the PAC script data at |filename|.
96  int SetPacScriptFromDisk(const char* filename) {
97    FilePath path;
98    PathService::Get(base::DIR_SOURCE_ROOT, &path);
99    path = path.AppendASCII("net");
100    path = path.AppendASCII("data");
101    path = path.AppendASCII("proxy_resolver_v8_unittest");
102    path = path.AppendASCII(filename);
103
104    // Try to read the file from disk.
105    std::string file_contents;
106    bool ok = file_util::ReadFileToString(path, &file_contents);
107
108    // If we can't load the file from disk, something is misconfigured.
109    if (!ok) {
110      LOG(ERROR) << "Failed to read file: " << path.value();
111      return ERR_UNEXPECTED;
112    }
113
114    // Load the PAC script into the ProxyResolver.
115    return SetPacScript(ProxyResolverScriptData::FromUTF8(file_contents),
116                        NULL);
117  }
118};
119
120// Doesn't really matter what these values are for many of the tests.
121const GURL kQueryUrl("http://www.google.com");
122const GURL kPacUrl;
123
124
125TEST(ProxyResolverV8Test, Direct) {
126  ProxyResolverV8WithMockBindings resolver;
127  int result = resolver.SetPacScriptFromDisk("direct.js");
128  EXPECT_EQ(OK, result);
129
130  ProxyInfo proxy_info;
131  CapturingBoundNetLog log(CapturingNetLog::kUnbounded);
132  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
133                                   log.bound());
134
135  EXPECT_EQ(OK, result);
136  EXPECT_TRUE(proxy_info.is_direct());
137
138  EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size());
139  EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size());
140
141  net::CapturingNetLog::EntryList entries;
142  log.GetEntries(&entries);
143  // No bindings were called, so no log entries.
144  EXPECT_EQ(0u, entries.size());
145}
146
147TEST(ProxyResolverV8Test, ReturnEmptyString) {
148  ProxyResolverV8WithMockBindings resolver;
149  int result = resolver.SetPacScriptFromDisk("return_empty_string.js");
150  EXPECT_EQ(OK, result);
151
152  ProxyInfo proxy_info;
153  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
154                                   BoundNetLog());
155
156  EXPECT_EQ(OK, result);
157  EXPECT_TRUE(proxy_info.is_direct());
158
159  EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size());
160  EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size());
161}
162
163TEST(ProxyResolverV8Test, Basic) {
164  ProxyResolverV8WithMockBindings resolver;
165  int result = resolver.SetPacScriptFromDisk("passthrough.js");
166  EXPECT_EQ(OK, result);
167
168  // The "FindProxyForURL" of this PAC script simply concatenates all of the
169  // arguments into a pseudo-host. The purpose of this test is to verify that
170  // the correct arguments are being passed to FindProxyForURL().
171  {
172    ProxyInfo proxy_info;
173    result = resolver.GetProxyForURL(GURL("http://query.com/path"),
174                                     &proxy_info, NULL, NULL, BoundNetLog());
175    EXPECT_EQ(OK, result);
176    EXPECT_EQ("http.query.com.path.query.com:80",
177              proxy_info.proxy_server().ToURI());
178  }
179  {
180    ProxyInfo proxy_info;
181    int result = resolver.GetProxyForURL(GURL("ftp://query.com:90/path"),
182                                         &proxy_info, NULL, NULL,
183                                         BoundNetLog());
184    EXPECT_EQ(OK, result);
185    // Note that FindProxyForURL(url, host) does not expect |host| to contain
186    // the port number.
187    EXPECT_EQ("ftp.query.com.90.path.query.com:80",
188              proxy_info.proxy_server().ToURI());
189
190    EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size());
191    EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size());
192  }
193
194  // We call this so we'll have code coverage of the function and valgrind will
195  // make sure nothing bad happens.
196  //
197  // NOTE: This is here instead of in its own test so that we'll be calling it
198  // after having done something, in hopes it won't be a no-op.
199  resolver.PurgeMemory();
200}
201
202TEST(ProxyResolverV8Test, BadReturnType) {
203  // These are the filenames of PAC scripts which each return a non-string
204  // types for FindProxyForURL(). They should all fail with
205  // ERR_PAC_SCRIPT_FAILED.
206  static const char* const filenames[] = {
207      "return_undefined.js",
208      "return_integer.js",
209      "return_function.js",
210      "return_object.js",
211      // TODO(eroman): Should 'null' be considered equivalent to "DIRECT" ?
212      "return_null.js"
213  };
214
215  for (size_t i = 0; i < arraysize(filenames); ++i) {
216    ProxyResolverV8WithMockBindings resolver;
217    int result = resolver.SetPacScriptFromDisk(filenames[i]);
218    EXPECT_EQ(OK, result);
219
220    ProxyInfo proxy_info;
221    result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
222                                     BoundNetLog());
223
224    EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result);
225
226    MockJSBindings* bindings = resolver.mock_js_bindings();
227    EXPECT_EQ(0U, bindings->alerts.size());
228    ASSERT_EQ(1U, bindings->errors.size());
229    EXPECT_EQ("FindProxyForURL() did not return a string.",
230              bindings->errors[0]);
231    EXPECT_EQ(-1, bindings->errors_line_number[0]);
232  }
233}
234
235// Try using a PAC script which defines no "FindProxyForURL" function.
236TEST(ProxyResolverV8Test, NoEntryPoint) {
237  ProxyResolverV8WithMockBindings resolver;
238  int result = resolver.SetPacScriptFromDisk("no_entrypoint.js");
239  EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result);
240
241  ProxyInfo proxy_info;
242  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
243                                   BoundNetLog());
244
245  EXPECT_EQ(ERR_FAILED, result);
246}
247
248// Try loading a malformed PAC script.
249TEST(ProxyResolverV8Test, ParseError) {
250  ProxyResolverV8WithMockBindings resolver;
251  int result = resolver.SetPacScriptFromDisk("missing_close_brace.js");
252  EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result);
253
254  ProxyInfo proxy_info;
255  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
256                                   BoundNetLog());
257
258  EXPECT_EQ(ERR_FAILED, result);
259
260  MockJSBindings* bindings = resolver.mock_js_bindings();
261  EXPECT_EQ(0U, bindings->alerts.size());
262
263  // We get one error during compilation.
264  ASSERT_EQ(1U, bindings->errors.size());
265
266  EXPECT_EQ("Uncaught SyntaxError: Unexpected end of input",
267            bindings->errors[0]);
268  EXPECT_EQ(0, bindings->errors_line_number[0]);
269}
270
271// Run a PAC script several times, which has side-effects.
272TEST(ProxyResolverV8Test, SideEffects) {
273  ProxyResolverV8WithMockBindings resolver;
274  int result = resolver.SetPacScriptFromDisk("side_effects.js");
275
276  // The PAC script increments a counter each time we invoke it.
277  for (int i = 0; i < 3; ++i) {
278    ProxyInfo proxy_info;
279    result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
280                                     BoundNetLog());
281    EXPECT_EQ(OK, result);
282    EXPECT_EQ(base::StringPrintf("sideffect_%d:80", i),
283              proxy_info.proxy_server().ToURI());
284  }
285
286  // Reload the script -- the javascript environment should be reset, hence
287  // the counter starts over.
288  result = resolver.SetPacScriptFromDisk("side_effects.js");
289  EXPECT_EQ(OK, result);
290
291  for (int i = 0; i < 3; ++i) {
292    ProxyInfo proxy_info;
293    result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
294                                     BoundNetLog());
295    EXPECT_EQ(OK, result);
296    EXPECT_EQ(base::StringPrintf("sideffect_%d:80", i),
297              proxy_info.proxy_server().ToURI());
298  }
299}
300
301// Execute a PAC script which throws an exception in FindProxyForURL.
302TEST(ProxyResolverV8Test, UnhandledException) {
303  ProxyResolverV8WithMockBindings resolver;
304  int result = resolver.SetPacScriptFromDisk("unhandled_exception.js");
305  EXPECT_EQ(OK, result);
306
307  ProxyInfo proxy_info;
308  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
309                                   BoundNetLog());
310
311  EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result);
312
313  MockJSBindings* bindings = resolver.mock_js_bindings();
314  EXPECT_EQ(0U, bindings->alerts.size());
315  ASSERT_EQ(1U, bindings->errors.size());
316  EXPECT_EQ("Uncaught ReferenceError: undefined_variable is not defined",
317            bindings->errors[0]);
318  EXPECT_EQ(3, bindings->errors_line_number[0]);
319}
320
321TEST(ProxyResolverV8Test, ReturnUnicode) {
322  ProxyResolverV8WithMockBindings resolver;
323  int result = resolver.SetPacScriptFromDisk("return_unicode.js");
324  EXPECT_EQ(OK, result);
325
326  ProxyInfo proxy_info;
327  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
328                                   BoundNetLog());
329
330  // The result from this resolve was unparseable, because it
331  // wasn't ASCII.
332  EXPECT_EQ(ERR_PAC_SCRIPT_FAILED, result);
333}
334
335// Test the PAC library functions that we expose in the JS environmnet.
336TEST(ProxyResolverV8Test, JavascriptLibrary) {
337  ProxyResolverV8WithMockBindings resolver;
338  int result = resolver.SetPacScriptFromDisk("pac_library_unittest.js");
339  EXPECT_EQ(OK, result);
340
341  ProxyInfo proxy_info;
342  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
343                                   BoundNetLog());
344
345  // If the javascript side of this unit-test fails, it will throw a javascript
346  // exception. Otherwise it will return "PROXY success:80".
347  EXPECT_EQ(OK, result);
348  EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI());
349
350  EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size());
351  EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size());
352}
353
354// Try resolving when SetPacScriptByData() has not been called.
355TEST(ProxyResolverV8Test, NoSetPacScript) {
356  ProxyResolverV8WithMockBindings resolver;
357
358  ProxyInfo proxy_info;
359
360  // Resolve should fail, as we are not yet initialized with a script.
361  int result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
362                                       BoundNetLog());
363  EXPECT_EQ(ERR_FAILED, result);
364
365  // Initialize it.
366  result = resolver.SetPacScriptFromDisk("direct.js");
367  EXPECT_EQ(OK, result);
368
369  // Resolve should now succeed.
370  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
371                                   BoundNetLog());
372  EXPECT_EQ(OK, result);
373
374  // Clear it, by initializing with an empty string.
375  resolver.SetPacScript(
376      ProxyResolverScriptData::FromUTF16(string16()), NULL);
377
378  // Resolve should fail again now.
379  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
380                                   BoundNetLog());
381  EXPECT_EQ(ERR_FAILED, result);
382
383  // Load a good script once more.
384  result = resolver.SetPacScriptFromDisk("direct.js");
385  EXPECT_EQ(OK, result);
386  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
387                                   BoundNetLog());
388  EXPECT_EQ(OK, result);
389
390  EXPECT_EQ(0U, resolver.mock_js_bindings()->alerts.size());
391  EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size());
392}
393
394// Test marshalling/un-marshalling of values between C++/V8.
395TEST(ProxyResolverV8Test, V8Bindings) {
396  ProxyResolverV8WithMockBindings resolver;
397  MockJSBindings* bindings = resolver.mock_js_bindings();
398  bindings->dns_resolve_result = "127.0.0.1";
399  int result = resolver.SetPacScriptFromDisk("bindings.js");
400  EXPECT_EQ(OK, result);
401
402  ProxyInfo proxy_info;
403  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
404                                   BoundNetLog());
405
406  EXPECT_EQ(OK, result);
407  EXPECT_TRUE(proxy_info.is_direct());
408
409  EXPECT_EQ(0U, resolver.mock_js_bindings()->errors.size());
410
411  // Alert was called 5 times.
412  ASSERT_EQ(5U, bindings->alerts.size());
413  EXPECT_EQ("undefined", bindings->alerts[0]);
414  EXPECT_EQ("null", bindings->alerts[1]);
415  EXPECT_EQ("undefined", bindings->alerts[2]);
416  EXPECT_EQ("[object Object]", bindings->alerts[3]);
417  EXPECT_EQ("exception from calling toString()", bindings->alerts[4]);
418
419  // DnsResolve was called 8 times, however only 2 of those were string
420  // parameters. (so 6 of them failed immediately).
421  ASSERT_EQ(2U, bindings->dns_resolves.size());
422  EXPECT_EQ("", bindings->dns_resolves[0]);
423  EXPECT_EQ("arg1", bindings->dns_resolves[1]);
424
425  // MyIpAddress was called two times.
426  EXPECT_EQ(2, bindings->my_ip_address_count);
427
428  // MyIpAddressEx was called once.
429  EXPECT_EQ(1, bindings->my_ip_address_ex_count);
430
431  // DnsResolveEx was called 2 times.
432  ASSERT_EQ(2U, bindings->dns_resolves_ex.size());
433  EXPECT_EQ("is_resolvable", bindings->dns_resolves_ex[0]);
434  EXPECT_EQ("foobar", bindings->dns_resolves_ex[1]);
435}
436
437// Test calling a binding (myIpAddress()) from the script's global scope.
438// http://crbug.com/40026
439TEST(ProxyResolverV8Test, BindingCalledDuringInitialization) {
440  ProxyResolverV8WithMockBindings resolver;
441
442  int result = resolver.SetPacScriptFromDisk("binding_from_global.js");
443  EXPECT_EQ(OK, result);
444
445  MockJSBindings* bindings = resolver.mock_js_bindings();
446
447  // myIpAddress() got called during initialization of the script.
448  EXPECT_EQ(1, bindings->my_ip_address_count);
449
450  ProxyInfo proxy_info;
451  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
452                                   BoundNetLog());
453
454  EXPECT_EQ(OK, result);
455  EXPECT_FALSE(proxy_info.is_direct());
456  EXPECT_EQ("127.0.0.1:80", proxy_info.proxy_server().ToURI());
457
458  // Check that no other bindings were called.
459  EXPECT_EQ(0U, bindings->errors.size());
460  ASSERT_EQ(0U, bindings->alerts.size());
461  ASSERT_EQ(0U, bindings->dns_resolves.size());
462  EXPECT_EQ(0, bindings->my_ip_address_ex_count);
463  ASSERT_EQ(0U, bindings->dns_resolves_ex.size());
464}
465
466// Try loading a PAC script that ends with a comment and has no terminal
467// newline. This should not cause problems with the PAC utility functions
468// that we add to the script's environment.
469// http://crbug.com/22864
470TEST(ProxyResolverV8Test, EndsWithCommentNoNewline) {
471  ProxyResolverV8WithMockBindings resolver;
472  int result = resolver.SetPacScriptFromDisk("ends_with_comment.js");
473  EXPECT_EQ(OK, result);
474
475  ProxyInfo proxy_info;
476  CapturingBoundNetLog log(CapturingNetLog::kUnbounded);
477  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
478                                   log.bound());
479
480  EXPECT_EQ(OK, result);
481  EXPECT_FALSE(proxy_info.is_direct());
482  EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI());
483}
484
485// Try loading a PAC script that ends with a statement and has no terminal
486// newline. This should not cause problems with the PAC utility functions
487// that we add to the script's environment.
488// http://crbug.com/22864
489TEST(ProxyResolverV8Test, EndsWithStatementNoNewline) {
490  ProxyResolverV8WithMockBindings resolver;
491  int result = resolver.SetPacScriptFromDisk(
492      "ends_with_statement_no_semicolon.js");
493  EXPECT_EQ(OK, result);
494
495  ProxyInfo proxy_info;
496  CapturingBoundNetLog log(CapturingNetLog::kUnbounded);
497  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
498                                   log.bound());
499
500  EXPECT_EQ(OK, result);
501  EXPECT_FALSE(proxy_info.is_direct());
502  EXPECT_EQ("success:3", proxy_info.proxy_server().ToURI());
503}
504
505// Test the return values from myIpAddress(), myIpAddressEx(), dnsResolve(),
506// dnsResolveEx(), isResolvable(), isResolvableEx(), when the the binding
507// returns empty string (failure). This simulates the return values from
508// those functions when the underlying DNS resolution fails.
509TEST(ProxyResolverV8Test, DNSResolutionFailure) {
510  ProxyResolverV8WithMockBindings resolver;
511  int result = resolver.SetPacScriptFromDisk("dns_fail.js");
512  EXPECT_EQ(OK, result);
513
514  ProxyInfo proxy_info;
515  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
516                                   BoundNetLog());
517
518  EXPECT_EQ(OK, result);
519  EXPECT_FALSE(proxy_info.is_direct());
520  EXPECT_EQ("success:80", proxy_info.proxy_server().ToURI());
521}
522
523TEST(ProxyResolverV8Test, DNSResolutionOfInternationDomainName) {
524  ProxyResolverV8WithMockBindings resolver;
525  int result = resolver.SetPacScriptFromDisk("international_domain_names.js");
526  EXPECT_EQ(OK, result);
527
528  // Execute FindProxyForURL().
529  ProxyInfo proxy_info;
530  result = resolver.GetProxyForURL(kQueryUrl, &proxy_info, NULL, NULL,
531                                   BoundNetLog());
532
533  EXPECT_EQ(OK, result);
534  EXPECT_TRUE(proxy_info.is_direct());
535
536  // Check that the international domain name was converted to punycode
537  // before passing it onto the bindings layer.
538  MockJSBindings* bindings = resolver.mock_js_bindings();
539
540  ASSERT_EQ(1u, bindings->dns_resolves.size());
541  EXPECT_EQ("xn--bcher-kva.ch", bindings->dns_resolves[0]);
542
543  ASSERT_EQ(1u, bindings->dns_resolves_ex.size());
544  EXPECT_EQ("xn--bcher-kva.ch", bindings->dns_resolves_ex[0]);
545}
546
547}  // namespace
548}  // namespace net
549