tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

extension.js (6183B)


      1 'use strict';
      2 
      3 const { tokenChars } = require('./validation');
      4 
      5 /**
      6 * Adds an offer to the map of extension offers or a parameter to the map of
      7 * parameters.
      8 *
      9 * @param {Object} dest The map of extension offers or parameters
     10 * @param {String} name The extension or parameter name
     11 * @param {(Object|Boolean|String)} elem The extension parameters or the
     12 *     parameter value
     13 * @private
     14 */
     15 function push(dest, name, elem) {
     16  if (dest[name] === undefined) dest[name] = [elem];
     17  else dest[name].push(elem);
     18 }
     19 
     20 /**
     21 * Parses the `Sec-WebSocket-Extensions` header into an object.
     22 *
     23 * @param {String} header The field value of the header
     24 * @return {Object} The parsed object
     25 * @public
     26 */
     27 function parse(header) {
     28  const offers = Object.create(null);
     29  let params = Object.create(null);
     30  let mustUnescape = false;
     31  let isEscaping = false;
     32  let inQuotes = false;
     33  let extensionName;
     34  let paramName;
     35  let start = -1;
     36  let code = -1;
     37  let end = -1;
     38  let i = 0;
     39 
     40  for (; i < header.length; i++) {
     41    code = header.charCodeAt(i);
     42 
     43    if (extensionName === undefined) {
     44      if (end === -1 && tokenChars[code] === 1) {
     45        if (start === -1) start = i;
     46      } else if (
     47        i !== 0 &&
     48        (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
     49      ) {
     50        if (end === -1 && start !== -1) end = i;
     51      } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
     52        if (start === -1) {
     53          throw new SyntaxError(`Unexpected character at index ${i}`);
     54        }
     55 
     56        if (end === -1) end = i;
     57        const name = header.slice(start, end);
     58        if (code === 0x2c) {
     59          push(offers, name, params);
     60          params = Object.create(null);
     61        } else {
     62          extensionName = name;
     63        }
     64 
     65        start = end = -1;
     66      } else {
     67        throw new SyntaxError(`Unexpected character at index ${i}`);
     68      }
     69    } else if (paramName === undefined) {
     70      if (end === -1 && tokenChars[code] === 1) {
     71        if (start === -1) start = i;
     72      } else if (code === 0x20 || code === 0x09) {
     73        if (end === -1 && start !== -1) end = i;
     74      } else if (code === 0x3b || code === 0x2c) {
     75        if (start === -1) {
     76          throw new SyntaxError(`Unexpected character at index ${i}`);
     77        }
     78 
     79        if (end === -1) end = i;
     80        push(params, header.slice(start, end), true);
     81        if (code === 0x2c) {
     82          push(offers, extensionName, params);
     83          params = Object.create(null);
     84          extensionName = undefined;
     85        }
     86 
     87        start = end = -1;
     88      } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
     89        paramName = header.slice(start, i);
     90        start = end = -1;
     91      } else {
     92        throw new SyntaxError(`Unexpected character at index ${i}`);
     93      }
     94    } else {
     95      //
     96      // The value of a quoted-string after unescaping must conform to the
     97      // token ABNF, so only token characters are valid.
     98      // Ref: https://tools.ietf.org/html/rfc6455#section-9.1
     99      //
    100      if (isEscaping) {
    101        if (tokenChars[code] !== 1) {
    102          throw new SyntaxError(`Unexpected character at index ${i}`);
    103        }
    104        if (start === -1) start = i;
    105        else if (!mustUnescape) mustUnescape = true;
    106        isEscaping = false;
    107      } else if (inQuotes) {
    108        if (tokenChars[code] === 1) {
    109          if (start === -1) start = i;
    110        } else if (code === 0x22 /* '"' */ && start !== -1) {
    111          inQuotes = false;
    112          end = i;
    113        } else if (code === 0x5c /* '\' */) {
    114          isEscaping = true;
    115        } else {
    116          throw new SyntaxError(`Unexpected character at index ${i}`);
    117        }
    118      } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
    119        inQuotes = true;
    120      } else if (end === -1 && tokenChars[code] === 1) {
    121        if (start === -1) start = i;
    122      } else if (start !== -1 && (code === 0x20 || code === 0x09)) {
    123        if (end === -1) end = i;
    124      } else if (code === 0x3b || code === 0x2c) {
    125        if (start === -1) {
    126          throw new SyntaxError(`Unexpected character at index ${i}`);
    127        }
    128 
    129        if (end === -1) end = i;
    130        let value = header.slice(start, end);
    131        if (mustUnescape) {
    132          value = value.replace(/\\/g, '');
    133          mustUnescape = false;
    134        }
    135        push(params, paramName, value);
    136        if (code === 0x2c) {
    137          push(offers, extensionName, params);
    138          params = Object.create(null);
    139          extensionName = undefined;
    140        }
    141 
    142        paramName = undefined;
    143        start = end = -1;
    144      } else {
    145        throw new SyntaxError(`Unexpected character at index ${i}`);
    146      }
    147    }
    148  }
    149 
    150  if (start === -1 || inQuotes || code === 0x20 || code === 0x09) {
    151    throw new SyntaxError('Unexpected end of input');
    152  }
    153 
    154  if (end === -1) end = i;
    155  const token = header.slice(start, end);
    156  if (extensionName === undefined) {
    157    push(offers, token, params);
    158  } else {
    159    if (paramName === undefined) {
    160      push(params, token, true);
    161    } else if (mustUnescape) {
    162      push(params, paramName, token.replace(/\\/g, ''));
    163    } else {
    164      push(params, paramName, token);
    165    }
    166    push(offers, extensionName, params);
    167  }
    168 
    169  return offers;
    170 }
    171 
    172 /**
    173 * Builds the `Sec-WebSocket-Extensions` header field value.
    174 *
    175 * @param {Object} extensions The map of extensions and parameters to format
    176 * @return {String} A string representing the given object
    177 * @public
    178 */
    179 function format(extensions) {
    180  return Object.keys(extensions)
    181    .map((extension) => {
    182      let configurations = extensions[extension];
    183      if (!Array.isArray(configurations)) configurations = [configurations];
    184      return configurations
    185        .map((params) => {
    186          return [extension]
    187            .concat(
    188              Object.keys(params).map((k) => {
    189                let values = params[k];
    190                if (!Array.isArray(values)) values = [values];
    191                return values
    192                  .map((v) => (v === true ? k : `${k}=${v}`))
    193                  .join('; ');
    194              })
    195            )
    196            .join('; ');
    197        })
    198        .join(', ');
    199    })
    200    .join(', ');
    201 }
    202 
    203 module.exports = { format, parse };