Home | History | Annotate | Download | only in net_internals
      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 
      7 var createLogEntryTablePrinter;
      8 var proxySettingsToString;
      9 var stripCookiesAndLoginInfo;
     10 
     11 // Start of anonymous namespace.
     12 (function() {
     13 'use strict';
     14 
     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);
     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  */
     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);
     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  */
    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('] ');
    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  */
    122 function 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  */
    156 var 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  */
    239 function 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  */
    272 function 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  */
    295 function 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  */
    361 function 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  */
    370 function getCertStatusFlagSymbolicString(certStatusFlag) {
    371   return getSymbolicString(certStatusFlag, CertStatusFlag, '');
    372 }
    373 
    374 /**
    375  * Returns a string representing the flags composing the given bitmask.
    376  */
    377 function 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  */
    396 function 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  */
    432 function 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  */
    441 function 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  */
    469 function 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  */
    544 stripCookiesAndLoginInfo = 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  */
    566 function 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  */
    585 function 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  */
    619 function 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 
    637 function 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 
    663 function 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 
    681 proxySettingsToString = 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