Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / net_internals / log_view_painter.js
blob4b29de3994db9154a66536ad657211c91b468e1e
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.
5 // TODO(eroman): put these methods into a namespace.
7 var createLogEntryTablePrinter;
8 var proxySettingsToString;
9 var stripCookiesAndLoginInfo;
11 // Start of anonymous namespace.
12 (function() {
13 'use strict';
15 function 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);
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  */
27 createLogEntryTablePrinter = function(logEntries, privacyStripping,
28                                       baseTime, logCreationTime) {
29   var entries = LogGroupEntry.createArrayFrom(logEntries);
30   var tablePrinter = new TablePrinter();
31   var parameterOutputter = new ParameterOutputter(tablePrinter);
33   if (entries.length == 0)
34     return tablePrinter;
36   var startTime = timeutil.convertTimeTicksToTime(entries[0].orig.time);
38   for (var i = 0; i < entries.length; ++i) {
39     var entry = entries[i];
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);
47       for (var j = entry.getDepth(); j > 0; --j)
48         tablePrinter.addCell('  ');
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       }
63       var mainCell = tablePrinter.addCell(eventText);
64       mainCell.allowOverflow = true;
65     }
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);
73       tablePrinter.setNewRowCellIndent(0);
74     }
75   }
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   }
89   return tablePrinter;
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  */
102 function 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('] ');
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.
119  * 16 bytes will be placed on each line of the output string, split into two
120  * columns of 8.
121  */
122 function writeHexString(hexString, out) {
123   var asciiCharsPerLine = 16;
124   // Number of transferred bytes in a line of output.  Length of a
125   // line is roughly 4 times larger.
126   var hexCharsPerLine = 2 * asciiCharsPerLine;
127   for (var i = 0; i < hexString.length; i += hexCharsPerLine) {
128     var hexLine = '';
129     var asciiLine = '';
130     for (var j = i; j < i + hexCharsPerLine && j < hexString.length; j += 2) {
131       // Split into two columns of 8 bytes each.
132       if (j == i + hexCharsPerLine / 2)
133         hexLine += ' ';
134       var hex = hexString.substr(j, 2);
135       hexLine += hex + ' ';
136       var charCode = parseInt(hex, 16);
137       // For ASCII codes 32 though 126, display the corresponding
138       // characters.  Use a space for nulls, and a period for
139       // everything else.
140       if (charCode >= 0x20 && charCode <= 0x7E) {
141         asciiLine += String.fromCharCode(charCode);
142       } else if (charCode == 0x00) {
143         asciiLine += ' ';
144       } else {
145         asciiLine += '.';
146       }
147     }
149     // Make the ASCII text for the last line of output align with the previous
150     // lines.
151     hexLine += makeRepeatedString(' ',
152                                   3 * asciiCharsPerLine + 1 - hexLine.length);
153     out.writeLine('   ' + hexLine + '  ' + asciiLine);
154   }
158  * Wrapper around a TablePrinter to simplify outputting lines of text for event
159  * parameters.
160  */
161 var ParameterOutputter = (function() {
162   /**
163    * @constructor
164    */
165   function ParameterOutputter(tablePrinter) {
166     this.tablePrinter_ = tablePrinter;
167   }
169   ParameterOutputter.prototype = {
170     /**
171      * Outputs a single line.
172      */
173     writeLine: function(line) {
174       this.tablePrinter_.addRow();
175       var cell = this.tablePrinter_.addCell(line);
176       cell.allowOverflow = true;
177       return cell;
178     },
180     /**
181      * Outputs a key=value line which looks like:
182      *
183      *   --> key = value
184      */
185     writeArrowKeyValue: function(key, value, link) {
186       var cell = this.writeLine(kArrow + key + ' = ' + value);
187       cell.link = link;
188     },
190     /**
191      * Outputs a key= line which looks like:
192      *
193      *   --> key =
194      */
195     writeArrowKey: function(key) {
196       this.writeLine(kArrow + key + ' =');
197     },
199     /**
200      * Outputs multiple lines, each indented by numSpaces.
201      * For instance if numSpaces=8 it might look like this:
202      *
203      *         line 1
204      *         line 2
205      *         line 3
206      */
207     writeSpaceIndentedLines: function(numSpaces, lines) {
208       var prefix = makeRepeatedString(' ', numSpaces);
209       for (var i = 0; i < lines.length; ++i)
210         this.writeLine(prefix + lines[i]);
211     },
213     /**
214      * Outputs multiple lines such that the first line has
215      * an arrow pointing at it, and subsequent lines
216      * align with the first one. For example:
217      *
218      *   --> line 1
219      *       line 2
220      *       line 3
221      */
222     writeArrowIndentedLines: function(lines) {
223       if (lines.length == 0)
224         return;
226       this.writeLine(kArrow + lines[0]);
228       for (var i = 1; i < lines.length; ++i)
229         this.writeLine(kArrowIndentation + lines[i]);
230     }
231   };
233   var kArrow = ' --> ';
234   var kArrowIndentation = '     ';
236   return ParameterOutputter;
237 })();  // end of ParameterOutputter
240  * Formats the parameters for |entry| and writes them to |out|.
241  * Certain event types have custom pretty printers. Everything else will
242  * default to a JSON-like format.
243  */
244 function writeParameters(entry, privacyStripping, out) {
245   if (privacyStripping) {
246     // If privacy stripping is enabled, remove data as needed.
247     entry = stripCookiesAndLoginInfo(entry);
248   } else {
249     // If headers are in an object, convert them to an array for better display.
250     entry = reformatHeaders(entry);
251   }
253   // Use any parameter writer available for this event type.
254   var paramsWriter = getParamaterWriterForEventType(entry.type);
255   var consumedParams = {};
256   if (paramsWriter)
257     paramsWriter(entry, out, consumedParams);
259   // Write any un-consumed parameters.
260   for (var k in entry.params) {
261     if (consumedParams[k])
262       continue;
263     defaultWriteParameter(k, entry.params[k], out);
264   }
268  * Finds a writer to format the parameters for events of type |eventType|.
270  * @return {function} The returned function "writer" can be invoked
271  *                    as |writer(entry, writer, consumedParams)|. It will
272  *                    output the parameters of |entry| to |out|, and fill
273  *                    |consumedParams| with the keys of the parameters
274  *                    consumed. If no writer is available for |eventType| then
275  *                    returns null.
276  */
277 function getParamaterWriterForEventType(eventType) {
278   switch (eventType) {
279     case EventType.HTTP_TRANSACTION_SEND_REQUEST_HEADERS:
280     case EventType.HTTP_TRANSACTION_SEND_TUNNEL_HEADERS:
281     case EventType.TYPE_HTTP_CACHE_CALLER_REQUEST_HEADERS:
282       return writeParamsForRequestHeaders;
284     case EventType.PROXY_CONFIG_CHANGED:
285       return writeParamsForProxyConfigChanged;
287     case EventType.CERT_VERIFIER_JOB:
288     case EventType.SSL_CERTIFICATES_RECEIVED:
289       return writeParamsForCertificates;
290     case EventType.EV_CERT_CT_COMPLIANCE_CHECKED:
291       return writeParamsForCheckedEVCertificates;
293     case EventType.SSL_VERSION_FALLBACK:
294       return writeParamsForSSLVersionFallback;
295   }
296   return null;
300  * Default parameter writer that outputs a visualization of field named |key|
301  * with value |value| to |out|.
302  */
303 function defaultWriteParameter(key, value, out) {
304   if (key == 'headers' && value instanceof Array) {
305     out.writeArrowIndentedLines(value);
306     return;
307   }
309   // For transferred bytes, display the bytes in hex and ASCII.
310   if (key == 'hex_encoded_bytes' && typeof value == 'string') {
311     out.writeArrowKey(key);
312     writeHexString(value, out);
313     return;
314   }
316   // Handle source_dependency entries - add link and map source type to
317   // string.
318   if (key == 'source_dependency' && typeof value == 'object') {
319     var link = '#events&s=' + value.id;
320     var valueStr = value.id + ' (' + EventSourceTypeNames[value.type] + ')';
321     out.writeArrowKeyValue(key, valueStr, link);
322     return;
323   }
325   if (key == 'net_error' && typeof value == 'number') {
326     var valueStr = value + ' (' + netErrorToString(value) + ')';
327     out.writeArrowKeyValue(key, valueStr);
328     return;
329   }
331   if (key == 'quic_error' && typeof value == 'number') {
332     var valueStr = value + ' (' + quicErrorToString(value) + ')';
333     out.writeArrowKeyValue(key, valueStr);
334     return;
335   }
337   if (key == 'quic_crypto_handshake_message' && typeof value == 'string') {
338     var lines = value.split('\n');
339     out.writeArrowIndentedLines(lines);
340     return;
341   }
343   if (key == 'quic_rst_stream_error' && typeof value == 'number') {
344     var valueStr = value + ' (' + quicRstStreamErrorToString(value) + ')';
345     out.writeArrowKeyValue(key, valueStr);
346     return;
347   }
349   if (key == 'load_flags' && typeof value == 'number') {
350     var valueStr = value + ' (' + getLoadFlagSymbolicString(value) + ')';
351     out.writeArrowKeyValue(key, valueStr);
352     return;
353   }
355   if (key == 'load_state' && typeof value == 'number') {
356     var valueStr = value + ' (' + getKeyWithValue(LoadState, value) + ')';
357     out.writeArrowKeyValue(key, valueStr);
358     return;
359   }
361   if (key == 'sdch_problem_code' && typeof value == 'number') {
362     var valueStr = value + ' (' + sdchProblemCodeToString(value) + ')';
363     out.writeArrowKeyValue(key, valueStr);
364     return;
365   }
367   // Otherwise just default to JSON formatting of the value.
368   out.writeArrowKeyValue(key, JSON.stringify(value));
372  * Returns the set of LoadFlags that make up the integer |loadFlag|.
373  * For example: getLoadFlagSymbolicString(
374  */
375 function getLoadFlagSymbolicString(loadFlag) {
377   return getSymbolicString(loadFlag, LoadFlag,
378                            getKeyWithValue(LoadFlag, loadFlag));
382  * Returns the set of CertStatusFlags that make up the integer |certStatusFlag|
383  */
384 function getCertStatusFlagSymbolicString(certStatusFlag) {
385   return getSymbolicString(certStatusFlag, CertStatusFlag, '');
389  * Returns a string representing the flags composing the given bitmask.
390  */
391 function getSymbolicString(bitmask, valueToName, zeroName) {
392   var matchingFlagNames = [];
394   for (var k in valueToName) {
395     if (bitmask & valueToName[k])
396       matchingFlagNames.push(k);
397   }
399   // If no flags were matched, returns a special value.
400   if (matchingFlagNames.length == 0)
401     return zeroName;
403   return matchingFlagNames.join(' | ');
407  * Converts an SSL version number to a textual representation.
408  * For instance, SSLVersionNumberToName(0x0301) returns 'TLS 1.0'.
409  */
410 function SSLVersionNumberToName(version) {
411   if ((version & 0xFFFF) != version) {
412     // If the version number is more than 2 bytes long something is wrong.
413     // Print it as hex.
414     return 'SSL 0x' + version.toString(16);
415   }
417   // See if it is a known TLS name.
418   var kTLSNames = {
419     0x0301: 'TLS 1.0',
420     0x0302: 'TLS 1.1',
421     0x0303: 'TLS 1.2'
422   };
423   var name = kTLSNames[version];
424   if (name)
425     return name;
427   // Otherwise label it as an SSL version.
428   var major = (version & 0xFF00) >> 8;
429   var minor = version & 0x00FF;
431   return 'SSL ' + major + '.' + minor;
435  * TODO(eroman): get rid of this, as it is only used by 1 callsite.
437  * Indent |lines| by |start|.
439  * For example, if |start| = ' -> ' and |lines| = ['line1', 'line2', 'line3']
440  * the output will be:
442  *   " -> line1\n" +
443  *   "    line2\n" +
444  *   "    line3"
445  */
446 function indentLines(start, lines) {
447   return start + lines.join('\n' + makeRepeatedString(' ', start.length));
451  * If entry.param.headers exists and is an object other than an array, converts
452  * it into an array and returns a new entry.  Otherwise, just returns the
453  * original entry.
454  */
455 function reformatHeaders(entry) {
456   // If there are no headers, or it is not an object other than an array,
457   // return |entry| without modification.
458   if (!entry.params || entry.params.headers === undefined ||
459       typeof entry.params.headers != 'object' ||
460       entry.params.headers instanceof Array) {
461     return entry;
462   }
464   // Duplicate the top level object, and |entry.params|, so the original object
465   // will not be modified.
466   entry = shallowCloneObject(entry);
467   entry.params = shallowCloneObject(entry.params);
469   // Convert headers to an array.
470   var headers = [];
471   for (var key in entry.params.headers)
472     headers.push(key + ': ' + entry.params.headers[key]);
473   entry.params.headers = headers;
475   return entry;
479  * Removes a cookie or unencrypted login information from a single HTTP header
480  * line, if present, and returns the modified line.  Otherwise, just returns
481  * the original line.
482  */
483 function stripCookieOrLoginInfo(line) {
484   var patterns = [
485       // Cookie patterns
486       /^set-cookie: /i,
487       /^set-cookie2: /i,
488       /^cookie: /i,
490       // Unencrypted authentication patterns
491       /^authorization: \S*\s*/i,
492       /^proxy-authorization: \S*\s*/i];
494   // Prefix will hold the first part of the string that contains no private
495   // information.  If null, no part of the string contains private information.
496   var prefix = null;
497   for (var i = 0; i < patterns.length; i++) {
498     var match = patterns[i].exec(line);
499     if (match != null) {
500       prefix = match[0];
501       break;
502     }
503   }
505   // Look for authentication information from data received from the server in
506   // multi-round Negotiate authentication.
507   if (prefix === null) {
508     var challengePatterns = [
509         /^www-authenticate: (\S*)\s*/i,
510         /^proxy-authenticate: (\S*)\s*/i];
511     for (var i = 0; i < challengePatterns.length; i++) {
512       var match = challengePatterns[i].exec(line);
513       if (!match)
514         continue;
516       // If there's no data after the scheme name, do nothing.
517       if (match[0].length == line.length)
518         break;
520       // Ignore lines with commas, as they may contain lists of schemes, and
521       // the information we want to hide is Base64 encoded, so has no commas.
522       if (line.indexOf(',') >= 0)
523         break;
525       // Ignore Basic and Digest authentication challenges, as they contain
526       // public information.
527       if (/^basic$/i.test(match[1]) || /^digest$/i.test(match[1]))
528         break;
530       prefix = match[0];
531       break;
532     }
533   }
535   if (prefix) {
536     var suffix = line.slice(prefix.length);
537     // If private information has already been removed, keep the line as-is.
538     // This is often the case when viewing a loaded log.
539     // TODO(mmenke):  Remove '[value was stripped]' check once M24 hits stable.
540     if (suffix.search(/^\[[0-9]+ bytes were stripped\]$/) == -1 &&
541         suffix != '[value was stripped]') {
542       return prefix + '[' + suffix.length + ' bytes were stripped]';
543     }
544   }
546   return line;
550  * If |entry| has headers, returns a copy of |entry| with all cookie and
551  * unencrypted login text removed.  Otherwise, returns original |entry| object.
552  * This is needed so that JSON log dumps can be made without affecting the
553  * source data.  Converts headers stored in objects to arrays.
555  * Note: this logic should be kept in sync with
556  * net::ElideHeaderForNetLog in net/http/http_log_util.cc.
557  */
558 stripCookiesAndLoginInfo = function(entry) {
559   if (!entry.params || entry.params.headers === undefined ||
560       !(entry.params.headers instanceof Object)) {
561     return entry;
562   }
564   // Make sure entry's headers are in an array.
565   entry = reformatHeaders(entry);
567   // Duplicate the top level object, and |entry.params|.  All other fields are
568   // just pointers to the original values, as they won't be modified, other than
569   // |entry.params.headers|.
570   entry = shallowCloneObject(entry);
571   entry.params = shallowCloneObject(entry.params);
573   entry.params.headers = entry.params.headers.map(stripCookieOrLoginInfo);
574   return entry;
578  * Outputs the request header parameters of |entry| to |out|.
579  */
580 function writeParamsForRequestHeaders(entry, out, consumedParams) {
581   var params = entry.params;
583   if (!(typeof params.line == 'string') || !(params.headers instanceof Array)) {
584     // Unrecognized params.
585     return;
586   }
588   // Strip the trailing CRLF that params.line contains.
589   var lineWithoutCRLF = params.line.replace(/\r\n$/g, '');
590   out.writeArrowIndentedLines([lineWithoutCRLF].concat(params.headers));
592   consumedParams.line = true;
593   consumedParams.headers = true;
596 function writeCertificateParam(
597     certs_container, out, consumedParams, paramName) {
598   if (certs_container.certificates instanceof Array) {
599     var certs = certs_container.certificates.reduce(
600         function(previous, current) {
601           return previous.concat(current.split('\n'));
602         }, new Array());
603     out.writeArrowKey(paramName);
604     out.writeSpaceIndentedLines(8, certs);
605     consumedParams[paramName] = true;
606   }
610  * Outputs the certificate parameters of |entry| to |out|.
611  */
612 function writeParamsForCertificates(entry, out, consumedParams) {
613   writeCertificateParam(entry.params, out, consumedParams, 'certificates');
615   if (typeof(entry.params.verified_cert) == 'object')
616     writeCertificateParam(
617         entry.params.verified_cert, out, consumedParams, 'verified_cert');
619   if (typeof(entry.params.cert_status) == 'number') {
620     var valueStr = entry.params.cert_status + ' (' +
621         getCertStatusFlagSymbolicString(entry.params.cert_status) + ')';
622     out.writeArrowKeyValue('cert_status', valueStr);
623     consumedParams.cert_status = true;
624   }
628 function writeParamsForCheckedEVCertificates(entry, out, consumedParams) {
629   if (typeof(entry.params.certificate) == 'object')
630     writeCertificateParam(
631         entry.params.certificate, out, consumedParams, 'certificate');
635  * Outputs the SSL version fallback parameters of |entry| to |out|.
636  */
637 function writeParamsForSSLVersionFallback(entry, out, consumedParams) {
638   var params = entry.params;
640   if (typeof params.version_before != 'number' ||
641       typeof params.version_after != 'number') {
642     // Unrecognized params.
643     return;
644   }
646   var line = SSLVersionNumberToName(params.version_before) +
647              ' ==> ' +
648              SSLVersionNumberToName(params.version_after);
649   out.writeArrowIndentedLines([line]);
651   consumedParams.version_before = true;
652   consumedParams.version_after = true;
655 function writeParamsForProxyConfigChanged(entry, out, consumedParams) {
656   var params = entry.params;
658   if (typeof params.new_config != 'object') {
659     // Unrecognized params.
660     return;
661   }
663   if (typeof params.old_config == 'object') {
664     var oldConfigString = proxySettingsToString(params.old_config);
665     // The previous configuration may not be present in the case of
666     // the initial proxy settings fetch.
667     out.writeArrowKey('old_config');
669     out.writeSpaceIndentedLines(8, oldConfigString.split('\n'));
671     consumedParams.old_config = true;
672   }
674   var newConfigString = proxySettingsToString(params.new_config);
675   out.writeArrowKey('new_config');
676   out.writeSpaceIndentedLines(8, newConfigString.split('\n'));
678   consumedParams.new_config = true;
681 function getTextForEvent(entry) {
682   var text = '';
684   if (entry.isBegin() && canCollapseBeginWithEnd(entry)) {
685     // Don't prefix with '+' if we are going to collapse the END event.
686     text = ' ';
687   } else if (entry.isBegin()) {
688     text = '+' + text;
689   } else if (entry.isEnd()) {
690     text = '-' + text;
691   } else {
692     text = ' ';
693   }
695   text += EventTypeNames[entry.orig.type];
696   return text;
699 proxySettingsToString = function(config) {
700   if (!config)
701     return '';
703   // TODO(eroman): if |config| has unexpected properties, print it as JSON
704   //               rather than hide them.
706   function getProxyListString(proxies) {
707     // Older versions of Chrome would set these values as strings, whereas newer
708     // logs use arrays.
709     // TODO(eroman): This behavior changed in M27. Support for older logs can
710     //               safely be removed circa M29.
711     if (Array.isArray(proxies)) {
712       var listString = proxies.join(', ');
713       if (proxies.length > 1)
714         return '[' + listString + ']';
715       return listString;
716     }
717     return proxies;
718   }
720   // The proxy settings specify up to three major fallback choices
721   // (auto-detect, custom pac url, or manual settings).
722   // We enumerate these to a list so we can later number them.
723   var modes = [];
725   // Output any automatic settings.
726   if (config.auto_detect)
727     modes.push(['Auto-detect']);
728   if (config.pac_url)
729     modes.push(['PAC script: ' + config.pac_url]);
731   // Output any manual settings.
732   if (config.single_proxy || config.proxy_per_scheme) {
733     var lines = [];
735     if (config.single_proxy) {
736       lines.push('Proxy server: ' + getProxyListString(config.single_proxy));
737     } else if (config.proxy_per_scheme) {
738       for (var urlScheme in config.proxy_per_scheme) {
739         if (urlScheme != 'fallback') {
740           lines.push('Proxy server for ' + urlScheme.toUpperCase() + ': ' +
741                      getProxyListString(config.proxy_per_scheme[urlScheme]));
742         }
743       }
744       if (config.proxy_per_scheme.fallback) {
745         lines.push('Proxy server for everything else: ' +
746                    getProxyListString(config.proxy_per_scheme.fallback));
747       }
748     }
750     // Output any proxy bypass rules.
751     if (config.bypass_list) {
752       if (config.reverse_bypass) {
753         lines.push('Reversed bypass list: ');
754       } else {
755         lines.push('Bypass list: ');
756       }
758       for (var i = 0; i < config.bypass_list.length; ++i)
759         lines.push('  ' + config.bypass_list[i]);
760     }
762     modes.push(lines);
763   }
765   var result = [];
766   if (modes.length < 1) {
767     // If we didn't find any proxy settings modes, we are using DIRECT.
768     result.push('Use DIRECT connections.');
769   } else if (modes.length == 1) {
770     // If there was just one mode, don't bother numbering it.
771     result.push(modes[0].join('\n'));
772   } else {
773     // Otherwise concatenate all of the modes into a numbered list
774     // (which correspond with the fallback order).
775     for (var i = 0; i < modes.length; ++i)
776       result.push(indentLines('(' + (i + 1) + ') ', modes[i]));
777   }
779   if (config.source != undefined && config.source != 'UNKNOWN')
780     result.push('Source: ' + config.source);
782   return result.join('\n');
785 // End of anonymous namespace.
786 })();