twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

extension.js (6814B)


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