1// Copyright (c) 2012 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// TODO(eroman): put these methods into a namespace.
6
7var createLogEntryTablePrinter;
8var proxySettingsToString;
9var stripCookiesAndLoginInfo;
10
11// Start of anonymous namespace.
12(function() {
13'use strict';
14
15function canCollapseBeginWithEnd(beginEntry) {
16  return beginEntry &&
17         beginEntry.isBegin() &&
18         beginEntry.end &&
19         beginEntry.end.index == beginEntry.index + 1 &&
20         (!beginEntry.orig.params || !beginEntry.end.orig.params);
21}
22
23/**
24 * Creates a TablePrinter for use by the above two functions.  baseTime is
25 * the time relative to which other times are displayed.
26 */
27createLogEntryTablePrinter = function(logEntries, privacyStripping,
28                                      baseTime, logCreationTime) {
29  var entries = LogGroupEntry.createArrayFrom(logEntries);
30  var tablePrinter = new TablePrinter();
31  var parameterOutputter = new ParameterOutputter(tablePrinter);
32
33  if (entries.length == 0)
34    return tablePrinter;
35
36  var startTime = timeutil.convertTimeTicksToTime(entries[0].orig.time);
37
38  for (var i = 0; i < entries.length; ++i) {
39    var entry = entries[i];
40
41    // Avoid printing the END for a BEGIN that was immediately before, unless
42    // both have extra parameters.
43    if (!entry.isEnd() || !canCollapseBeginWithEnd(entry.begin)) {
44      var entryTime = timeutil.convertTimeTicksToTime(entry.orig.time);
45      addRowWithTime(tablePrinter, entryTime - baseTime, startTime - baseTime);
46
47      for (var j = entry.getDepth(); j > 0; --j)
48        tablePrinter.addCell('  ');
49
50      var eventText = getTextForEvent(entry);
51      // Get the elapsed time, and append it to the event text.
52      if (entry.isBegin()) {
53        var dt = '?';
54        // Definite time.
55        if (entry.end) {
56          dt = entry.end.orig.time - entry.orig.time;
57        } else if (logCreationTime != undefined) {
58          dt = (logCreationTime - entryTime) + '+';
59        }
60        eventText += '  [dt=' + dt + ']';
61      }
62
63      var mainCell = tablePrinter.addCell(eventText);
64      mainCell.allowOverflow = true;
65    }
66
67    // Output the extra parameters.
68    if (typeof entry.orig.params == 'object') {
69      // Those 5 skipped cells are: two for "t=", and three for "st=".
70      tablePrinter.setNewRowCellIndent(5 + entry.getDepth());
71      writeParameters(entry.orig, privacyStripping, parameterOutputter);
72
73      tablePrinter.setNewRowCellIndent(0);
74    }
75  }
76
77  // If viewing a saved log file, add row with just the time the log was
78  // created, if the event never completed.
79  var lastEntry = entries[entries.length - 1];
80  // If the last entry has a non-zero depth or is a begin event, the source is
81  // still active.
82  var isSourceActive = lastEntry.getDepth() != 0 || lastEntry.isBegin();
83  if (logCreationTime != undefined && isSourceActive) {
84    addRowWithTime(tablePrinter,
85                   logCreationTime - baseTime,
86                   startTime - baseTime);
87  }
88
89  return tablePrinter;
90}
91
92/**
93 * Adds a new row to the given TablePrinter, and adds five cells containing
94 * information about the time an event occured.
95 * Format is '[t=<time of the event in ms>] [st=<ms since the source started>]'.
96 * @param {TablePrinter} tablePrinter The table printer to add the cells to.
97 * @param {number} eventTime The time the event occured, in milliseconds,
98 *     relative to some base time.
99 * @param {number} startTime The time the first event for the source occured,
100 *     relative to the same base time as eventTime.
101 */
102function addRowWithTime(tablePrinter, eventTime, startTime) {
103  tablePrinter.addRow();
104  tablePrinter.addCell('t=');
105  var tCell = tablePrinter.addCell(eventTime);
106  tCell.alignRight = true;
107  tablePrinter.addCell(' [st=');
108  var stCell = tablePrinter.addCell(eventTime - startTime);
109  stCell.alignRight = true;
110  tablePrinter.addCell('] ');
111}
112
113/**
114 * |hexString| must be a string of hexadecimal characters with no whitespace,
115 * whose length is a multiple of two.  Writes multiple lines to |out| with
116 * the hexadecimal characters from |hexString| on the left, in groups of
117 * two, and their corresponding ASCII characters on the right.
118 *
119 * |asciiCharsPerLine| specifies how many ASCII characters will be put on each
120 * line of the output string.
121 */
122function writeHexString(hexString, asciiCharsPerLine, out) {
123  // Number of transferred bytes in a line of output.  Length of a
124  // line is roughly 4 times larger.
125  var hexCharsPerLine = 2 * asciiCharsPerLine;
126  for (var i = 0; i < hexString.length; i += hexCharsPerLine) {
127    var hexLine = '';
128    var asciiLine = '';
129    for (var j = i; j < i + hexCharsPerLine && j < hexString.length; j += 2) {
130      var hex = hexString.substr(j, 2);
131      hexLine += hex + ' ';
132      var charCode = parseInt(hex, 16);
133      // For ASCII codes 32 though 126, display the corresponding
134      // characters.  Use a space for nulls, and a period for
135      // everything else.
136      if (charCode >= 0x20 && charCode <= 0x7E) {
137        asciiLine += String.fromCharCode(charCode);
138      } else if (charCode == 0x00) {
139        asciiLine += ' ';
140      } else {
141        asciiLine += '.';
142      }
143    }
144
145    // Make the ASCII text for the last line of output align with the previous
146    // lines.
147    hexLine += makeRepeatedString(' ', 3 * asciiCharsPerLine - hexLine.length);
148    out.writeLine('   ' + hexLine + '  ' + asciiLine);
149  }
150}
151
152/**
153 * Wrapper around a TablePrinter to simplify outputting lines of text for event
154 * parameters.
155 */
156var ParameterOutputter = (function() {
157  /**
158   * @constructor
159   */
160  function ParameterOutputter(tablePrinter) {
161    this.tablePrinter_ = tablePrinter;
162  }
163
164  ParameterOutputter.prototype = {
165    /**
166     * Outputs a single line.
167     */
168    writeLine: function(line) {
169      this.tablePrinter_.addRow();
170      var cell = this.tablePrinter_.addCell(line);
171      cell.allowOverflow = true;
172      return cell;
173    },
174
175    /**
176     * Outputs a key=value line which looks like:
177     *
178     *   --> key = value
179     */
180    writeArrowKeyValue: function(key, value, link) {
181      var cell = this.writeLine(kArrow + key + ' = ' + value);
182      cell.link = link;
183    },
184
185    /**
186     * Outputs a key= line which looks like:
187     *
188     *   --> key =
189     */
190    writeArrowKey: function(key) {
191      this.writeLine(kArrow + key + ' =');
192    },
193
194    /**
195     * Outputs multiple lines, each indented by numSpaces.
196     * For instance if numSpaces=8 it might look like this:
197     *
198     *         line 1
199     *         line 2
200     *         line 3
201     */
202    writeSpaceIndentedLines: function(numSpaces, lines) {
203      var prefix = makeRepeatedString(' ', numSpaces);
204      for (var i = 0; i < lines.length; ++i)
205        this.writeLine(prefix + lines[i]);
206    },
207
208    /**
209     * Outputs multiple lines such that the first line has
210     * an arrow pointing at it, and subsequent lines
211     * align with the first one. For example:
212     *
213     *   --> line 1
214     *       line 2
215     *       line 3
216     */
217    writeArrowIndentedLines: function(lines) {
218      if (lines.length == 0)
219        return;
220
221      this.writeLine(kArrow + lines[0]);
222
223      for (var i = 1; i < lines.length; ++i)
224        this.writeLine(kArrowIndentation + lines[i]);
225    }
226  };
227
228  var kArrow = ' --> ';
229  var kArrowIndentation = '     ';
230
231  return ParameterOutputter;
232})();  // end of ParameterOutputter
233
234/**
235 * Formats the parameters for |entry| and writes them to |out|.
236 * Certain event types have custom pretty printers. Everything else will
237 * default to a JSON-like format.
238 */
239function writeParameters(entry, privacyStripping, out) {
240  if (privacyStripping) {
241    // If privacy stripping is enabled, remove data as needed.
242    entry = stripCookiesAndLoginInfo(entry);
243  } else {
244    // If headers are in an object, convert them to an array for better display.
245    entry = reformatHeaders(entry);
246  }
247
248  // Use any parameter writer available for this event type.
249  var paramsWriter = getParamaterWriterForEventType(entry.type);
250  var consumedParams = {};
251  if (paramsWriter)
252    paramsWriter(entry, out, consumedParams);
253
254  // Write any un-consumed parameters.
255  for (var k in entry.params) {
256    if (consumedParams[k])
257      continue;
258    defaultWriteParameter(k, entry.params[k], out);
259  }
260}
261
262/**
263 * Finds a writer to format the parameters for events of type |eventType|.
264 *
265 * @return {function} The returned function "writer" can be invoked
266 *                    as |writer(entry, writer, consumedParams)|. It will
267 *                    output the parameters of |entry| to |out|, and fill
268 *                    |consumedParams| with the keys of the parameters
269 *                    consumed. If no writer is available for |eventType| then
270 *                    returns null.
271 */
272function getParamaterWriterForEventType(eventType) {
273  switch (eventType) {
274    case EventType.HTTP_TRANSACTION_SEND_REQUEST_HEADERS:
275    case EventType.HTTP_TRANSACTION_SEND_TUNNEL_HEADERS:
276      return writeParamsForRequestHeaders;
277
278    case EventType.PROXY_CONFIG_CHANGED:
279      return writeParamsForProxyConfigChanged;
280
281    case EventType.CERT_VERIFIER_JOB:
282    case EventType.SSL_CERTIFICATES_RECEIVED:
283      return writeParamsForCertificates;
284
285    case EventType.SSL_VERSION_FALLBACK:
286      return writeParamsForSSLVersionFallback;
287  }
288  return null;
289}
290
291/**
292 * Default parameter writer that outputs a visualization of field named |key|
293 * with value |value| to |out|.
294 */
295function defaultWriteParameter(key, value, out) {
296  if (key == 'headers' && value instanceof Array) {
297    out.writeArrowIndentedLines(value);
298    return;
299  }
300
301  // For transferred bytes, display the bytes in hex and ASCII.
302  if (key == 'hex_encoded_bytes' && typeof value == 'string') {
303    out.writeArrowKey(key);
304    writeHexString(value, 20, out);
305    return;
306  }
307
308  // Handle source_dependency entries - add link and map source type to
309  // string.
310  if (key == 'source_dependency' && typeof value == 'object') {
311    var link = '#events&s=' + value.id;
312    var valueStr = value.id + ' (' + EventSourceTypeNames[value.type] + ')';
313    out.writeArrowKeyValue(key, valueStr, link);
314    return;
315  }
316
317  if (key == 'net_error' && typeof value == 'number') {
318    var valueStr = value + ' (' + netErrorToString(value) + ')';
319    out.writeArrowKeyValue(key, valueStr);
320    return;
321  }
322
323  if (key == 'quic_error' && typeof value == 'number') {
324    var valueStr = value + ' (' + quicErrorToString(value) + ')';
325    out.writeArrowKeyValue(key, valueStr);
326    return;
327  }
328
329  if (key == 'quic_crypto_handshake_message' && typeof value == 'string') {
330    var lines = value.split('\n');
331    out.writeArrowIndentedLines(lines);
332    return;
333  }
334
335  if (key == 'quic_rst_stream_error' && typeof value == 'number') {
336    var valueStr = value + ' (' + quicRstStreamErrorToString(value) + ')';
337    out.writeArrowKeyValue(key, valueStr);
338    return;
339  }
340
341  if (key == 'load_flags' && typeof value == 'number') {
342    var valueStr = value + ' (' + getLoadFlagSymbolicString(value) + ')';
343    out.writeArrowKeyValue(key, valueStr);
344    return;
345  }
346
347  if (key == 'load_state' && typeof value == 'number') {
348    var valueStr = value + ' (' + getKeyWithValue(LoadState, value) + ')';
349    out.writeArrowKeyValue(key, valueStr);
350    return;
351  }
352
353  // Otherwise just default to JSON formatting of the value.
354  out.writeArrowKeyValue(key, JSON.stringify(value));
355}
356
357/**
358 * Returns the set of LoadFlags that make up the integer |loadFlag|.
359 * For example: getLoadFlagSymbolicString(
360 */
361function getLoadFlagSymbolicString(loadFlag) {
362
363  return getSymbolicString(loadFlag, LoadFlag,
364                           getKeyWithValue(LoadFlag, loadFlag));
365}
366
367/**
368 * Returns the set of CertStatusFlags that make up the integer |certStatusFlag|
369 */
370function getCertStatusFlagSymbolicString(certStatusFlag) {
371  return getSymbolicString(certStatusFlag, CertStatusFlag, '');
372}
373
374/**
375 * Returns a string representing the flags composing the given bitmask.
376 */
377function getSymbolicString(bitmask, valueToName, zeroName) {
378  var matchingFlagNames = [];
379
380  for (var k in valueToName) {
381    if (bitmask & valueToName[k])
382      matchingFlagNames.push(k);
383  }
384
385  // If no flags were matched, returns a special value.
386  if (matchingFlagNames.length == 0)
387    return zeroName;
388
389  return matchingFlagNames.join(' | ');
390}
391
392/**
393 * Converts an SSL version number to a textual representation.
394 * For instance, SSLVersionNumberToName(0x0301) returns 'TLS 1.0'.
395 */
396function SSLVersionNumberToName(version) {
397  if ((version & 0xFFFF) != version) {
398    // If the version number is more than 2 bytes long something is wrong.
399    // Print it as hex.
400    return 'SSL 0x' + version.toString(16);
401  }
402
403  // See if it is a known TLS name.
404  var kTLSNames = {
405    0x0301: 'TLS 1.0',
406    0x0302: 'TLS 1.1',
407    0x0303: 'TLS 1.2'
408  };
409  var name = kTLSNames[version];
410  if (name)
411    return name;
412
413  // Otherwise label it as an SSL version.
414  var major = (version & 0xFF00) >> 8;
415  var minor = version & 0x00FF;
416
417  return 'SSL ' + major + '.' + minor;
418}
419
420/**
421 * TODO(eroman): get rid of this, as it is only used by 1 callsite.
422 *
423 * Indent |lines| by |start|.
424 *
425 * For example, if |start| = ' -> ' and |lines| = ['line1', 'line2', 'line3']
426 * the output will be:
427 *
428 *   " -> line1\n" +
429 *   "    line2\n" +
430 *   "    line3"
431 */
432function indentLines(start, lines) {
433  return start + lines.join('\n' + makeRepeatedString(' ', start.length));
434}
435
436/**
437 * If entry.param.headers exists and is an object other than an array, converts
438 * it into an array and returns a new entry.  Otherwise, just returns the
439 * original entry.
440 */
441function reformatHeaders(entry) {
442  // If there are no headers, or it is not an object other than an array,
443  // return |entry| without modification.
444  if (!entry.params || entry.params.headers === undefined ||
445      typeof entry.params.headers != 'object' ||
446      entry.params.headers instanceof Array) {
447    return entry;
448  }
449
450  // Duplicate the top level object, and |entry.params|, so the original object
451  // will not be modified.
452  entry = shallowCloneObject(entry);
453  entry.params = shallowCloneObject(entry.params);
454
455  // Convert headers to an array.
456  var headers = [];
457  for (var key in entry.params.headers)
458    headers.push(key + ': ' + entry.params.headers[key]);
459  entry.params.headers = headers;
460
461  return entry;
462}
463
464/**
465 * Removes a cookie or unencrypted login information from a single HTTP header
466 * line, if present, and returns the modified line.  Otherwise, just returns
467 * the original line.
468 */
469function stripCookieOrLoginInfo(line) {
470  var patterns = [
471      // Cookie patterns
472      /^set-cookie: /i,
473      /^set-cookie2: /i,
474      /^cookie: /i,
475
476      // Unencrypted authentication patterns
477      /^authorization: \S*\s*/i,
478      /^proxy-authorization: \S*\s*/i];
479
480  // Prefix will hold the first part of the string that contains no private
481  // information.  If null, no part of the string contains private information.
482  var prefix = null;
483  for (var i = 0; i < patterns.length; i++) {
484    var match = patterns[i].exec(line);
485    if (match != null) {
486      prefix = match[0];
487      break;
488    }
489  }
490
491  // Look for authentication information from data received from the server in
492  // multi-round Negotiate authentication.
493  if (prefix === null) {
494    var challengePatterns = [
495        /^www-authenticate: (\S*)\s*/i,
496        /^proxy-authenticate: (\S*)\s*/i];
497    for (var i = 0; i < challengePatterns.length; i++) {
498      var match = challengePatterns[i].exec(line);
499      if (!match)
500        continue;
501
502      // If there's no data after the scheme name, do nothing.
503      if (match[0].length == line.length)
504        break;
505
506      // Ignore lines with commas, as they may contain lists of schemes, and
507      // the information we want to hide is Base64 encoded, so has no commas.
508      if (line.indexOf(',') >= 0)
509        break;
510
511      // Ignore Basic and Digest authentication challenges, as they contain
512      // public information.
513      if (/^basic$/i.test(match[1]) || /^digest$/i.test(match[1]))
514        break;
515
516      prefix = match[0];
517      break;
518    }
519  }
520
521  if (prefix) {
522    var suffix = line.slice(prefix.length);
523    // If private information has already been removed, keep the line as-is.
524    // This is often the case when viewing a loaded log.
525    // TODO(mmenke):  Remove '[value was stripped]' check once M24 hits stable.
526    if (suffix.search(/^\[[0-9]+ bytes were stripped\]$/) == -1 &&
527        suffix != '[value was stripped]') {
528      return prefix + '[' + suffix.length + ' bytes were stripped]';
529    }
530  }
531
532  return line;
533}
534
535/**
536 * If |entry| has headers, returns a copy of |entry| with all cookie and
537 * unencrypted login text removed.  Otherwise, returns original |entry| object.
538 * This is needed so that JSON log dumps can be made without affecting the
539 * source data.  Converts headers stored in objects to arrays.
540 *
541 * Note: this logic should be kept in sync with
542 * net::ElideHeaderForNetLog in net/http/http_log_util.cc.
543 */
544stripCookiesAndLoginInfo = function(entry) {
545  if (!entry.params || entry.params.headers === undefined ||
546      !(entry.params.headers instanceof Object)) {
547    return entry;
548  }
549
550  // Make sure entry's headers are in an array.
551  entry = reformatHeaders(entry);
552
553  // Duplicate the top level object, and |entry.params|.  All other fields are
554  // just pointers to the original values, as they won't be modified, other than
555  // |entry.params.headers|.
556  entry = shallowCloneObject(entry);
557  entry.params = shallowCloneObject(entry.params);
558
559  entry.params.headers = entry.params.headers.map(stripCookieOrLoginInfo);
560  return entry;
561}
562
563/**
564 * Outputs the request header parameters of |entry| to |out|.
565 */
566function writeParamsForRequestHeaders(entry, out, consumedParams) {
567  var params = entry.params;
568
569  if (!(typeof params.line == 'string') || !(params.headers instanceof Array)) {
570    // Unrecognized params.
571    return;
572  }
573
574  // Strip the trailing CRLF that params.line contains.
575  var lineWithoutCRLF = params.line.replace(/\r\n$/g, '');
576  out.writeArrowIndentedLines([lineWithoutCRLF].concat(params.headers));
577
578  consumedParams.line = true;
579  consumedParams.headers = true;
580}
581
582/**
583 * Outputs the certificate parameters of |entry| to |out|.
584 */
585function writeParamsForCertificates(entry, out, consumedParams) {
586  if (entry.params.certificates instanceof Array) {
587    var certs = entry.params.certificates.reduce(function(previous, current) {
588      return previous.concat(current.split('\n'));
589    }, new Array());
590    out.writeArrowKey('certificates');
591    out.writeSpaceIndentedLines(8, certs);
592    consumedParams.certificates = true;
593  }
594
595  if (typeof(entry.params.verified_cert) == 'object') {
596    if (entry.params.verified_cert.certificates instanceof Array) {
597      var certs = entry.params.verified_cert.certificates.reduce(
598          function(previous, current) {
599        return previous.concat(current.split('\n'));
600      }, new Array());
601      out.writeArrowKey('verified_cert');
602      out.writeSpaceIndentedLines(8, certs);
603      consumedParams.verified_cert = true;
604    }
605  }
606
607  if (typeof(entry.params.cert_status) == 'number') {
608    var valueStr = entry.params.cert_status + ' (' +
609        getCertStatusFlagSymbolicString(entry.params.cert_status) + ')';
610    out.writeArrowKeyValue('cert_status', valueStr);
611    consumedParams.cert_status = true;
612  }
613
614}
615
616/**
617 * Outputs the SSL version fallback parameters of |entry| to |out|.
618 */
619function writeParamsForSSLVersionFallback(entry, out, consumedParams) {
620  var params = entry.params;
621
622  if (typeof params.version_before != 'number' ||
623      typeof params.version_after != 'number') {
624    // Unrecognized params.
625    return;
626  }
627
628  var line = SSLVersionNumberToName(params.version_before) +
629             ' ==> ' +
630             SSLVersionNumberToName(params.version_after);
631  out.writeArrowIndentedLines([line]);
632
633  consumedParams.version_before = true;
634  consumedParams.version_after = true;
635}
636
637function writeParamsForProxyConfigChanged(entry, out, consumedParams) {
638  var params = entry.params;
639
640  if (typeof params.new_config != 'object') {
641    // Unrecognized params.
642    return;
643  }
644
645  if (typeof params.old_config == 'object') {
646    var oldConfigString = proxySettingsToString(params.old_config);
647    // The previous configuration may not be present in the case of
648    // the initial proxy settings fetch.
649    out.writeArrowKey('old_config');
650
651    out.writeSpaceIndentedLines(8, oldConfigString.split('\n'));
652
653    consumedParams.old_config = true;
654  }
655
656  var newConfigString = proxySettingsToString(params.new_config);
657  out.writeArrowKey('new_config');
658  out.writeSpaceIndentedLines(8, newConfigString.split('\n'));
659
660  consumedParams.new_config = true;
661}
662
663function getTextForEvent(entry) {
664  var text = '';
665
666  if (entry.isBegin() && canCollapseBeginWithEnd(entry)) {
667    // Don't prefix with '+' if we are going to collapse the END event.
668    text = ' ';
669  } else if (entry.isBegin()) {
670    text = '+' + text;
671  } else if (entry.isEnd()) {
672    text = '-' + text;
673  } else {
674    text = ' ';
675  }
676
677  text += EventTypeNames[entry.orig.type];
678  return text;
679}
680
681proxySettingsToString = function(config) {
682  if (!config)
683    return '';
684
685  // TODO(eroman): if |config| has unexpected properties, print it as JSON
686  //               rather than hide them.
687
688  function getProxyListString(proxies) {
689    // Older versions of Chrome would set these values as strings, whereas newer
690    // logs use arrays.
691    // TODO(eroman): This behavior changed in M27. Support for older logs can
692    //               safely be removed circa M29.
693    if (Array.isArray(proxies)) {
694      var listString = proxies.join(', ');
695      if (proxies.length > 1)
696        return '[' + listString + ']';
697      return listString;
698    }
699    return proxies;
700  }
701
702  // The proxy settings specify up to three major fallback choices
703  // (auto-detect, custom pac url, or manual settings).
704  // We enumerate these to a list so we can later number them.
705  var modes = [];
706
707  // Output any automatic settings.
708  if (config.auto_detect)
709    modes.push(['Auto-detect']);
710  if (config.pac_url)
711    modes.push(['PAC script: ' + config.pac_url]);
712
713  // Output any manual settings.
714  if (config.single_proxy || config.proxy_per_scheme) {
715    var lines = [];
716
717    if (config.single_proxy) {
718      lines.push('Proxy server: ' + getProxyListString(config.single_proxy));
719    } else if (config.proxy_per_scheme) {
720      for (var urlScheme in config.proxy_per_scheme) {
721        if (urlScheme != 'fallback') {
722          lines.push('Proxy server for ' + urlScheme.toUpperCase() + ': ' +
723                     getProxyListString(config.proxy_per_scheme[urlScheme]));
724        }
725      }
726      if (config.proxy_per_scheme.fallback) {
727        lines.push('Proxy server for everything else: ' +
728                   getProxyListString(config.proxy_per_scheme.fallback));
729      }
730    }
731
732    // Output any proxy bypass rules.
733    if (config.bypass_list) {
734      if (config.reverse_bypass) {
735        lines.push('Reversed bypass list: ');
736      } else {
737        lines.push('Bypass list: ');
738      }
739
740      for (var i = 0; i < config.bypass_list.length; ++i)
741        lines.push('  ' + config.bypass_list[i]);
742    }
743
744    modes.push(lines);
745  }
746
747  var result = [];
748  if (modes.length < 1) {
749    // If we didn't find any proxy settings modes, we are using DIRECT.
750    result.push('Use DIRECT connections.');
751  } else if (modes.length == 1) {
752    // If there was just one mode, don't bother numbering it.
753    result.push(modes[0].join('\n'));
754  } else {
755    // Otherwise concatenate all of the modes into a numbered list
756    // (which correspond with the fallback order).
757    for (var i = 0; i < modes.length; ++i)
758      result.push(indentLines('(' + (i + 1) + ') ', modes[i]));
759  }
760
761  if (config.source != undefined && config.source != 'UNKNOWN')
762    result.push('Source: ' + config.source);
763
764  return result.join('\n');
765};
766
767// End of anonymous namespace.
768})();
769