buddy

node MVC discord bot
Log | Files | Refs | README

extension.js (6883B)


      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 (dest[name] === undefined) dest[name] = [elem];
     38   else dest[name].push(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 = Object.create(null);
     50 
     51   if (header === undefined || header === '') return offers;
     52 
     53   let params = Object.create(null);
     54   let mustUnescape = false;
     55   let isEscaping = false;
     56   let inQuotes = false;
     57   let extensionName;
     58   let paramName;
     59   let start = -1;
     60   let end = -1;
     61   let i = 0;
     62 
     63   for (; i < header.length; i++) {
     64     const code = header.charCodeAt(i);
     65 
     66     if (extensionName === undefined) {
     67       if (end === -1 && tokenChars[code] === 1) {
     68         if (start === -1) start = i;
     69       } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) {
     70         if (end === -1 && start !== -1) end = i;
     71       } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
     72         if (start === -1) {
     73           throw new SyntaxError(`Unexpected character at index ${i}`);
     74         }
     75 
     76         if (end === -1) end = i;
     77         const name = header.slice(start, end);
     78         if (code === 0x2c) {
     79           push(offers, name, params);
     80           params = Object.create(null);
     81         } else {
     82           extensionName = name;
     83         }
     84 
     85         start = end = -1;
     86       } else {
     87         throw new SyntaxError(`Unexpected character at index ${i}`);
     88       }
     89     } else if (paramName === undefined) {
     90       if (end === -1 && tokenChars[code] === 1) {
     91         if (start === -1) start = i;
     92       } else if (code === 0x20 || code === 0x09) {
     93         if (end === -1 && start !== -1) end = i;
     94       } else if (code === 0x3b || code === 0x2c) {
     95         if (start === -1) {
     96           throw new SyntaxError(`Unexpected character at index ${i}`);
     97         }
     98 
     99         if (end === -1) end = i;
    100         push(params, header.slice(start, end), true);
    101         if (code === 0x2c) {
    102           push(offers, extensionName, params);
    103           params = Object.create(null);
    104           extensionName = undefined;
    105         }
    106 
    107         start = end = -1;
    108       } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
    109         paramName = header.slice(start, i);
    110         start = end = -1;
    111       } else {
    112         throw new SyntaxError(`Unexpected character at index ${i}`);
    113       }
    114     } else {
    115       //
    116       // The value of a quoted-string after unescaping must conform to the
    117       // token ABNF, so only token characters are valid.
    118       // Ref: https://tools.ietf.org/html/rfc6455#section-9.1
    119       //
    120       if (isEscaping) {
    121         if (tokenChars[code] !== 1) {
    122           throw new SyntaxError(`Unexpected character at index ${i}`);
    123         }
    124         if (start === -1) start = i;
    125         else if (!mustUnescape) mustUnescape = true;
    126         isEscaping = false;
    127       } else if (inQuotes) {
    128         if (tokenChars[code] === 1) {
    129           if (start === -1) start = i;
    130         } else if (code === 0x22 /* '"' */ && start !== -1) {
    131           inQuotes = false;
    132           end = i;
    133         } else if (code === 0x5c /* '\' */) {
    134           isEscaping = true;
    135         } else {
    136           throw new SyntaxError(`Unexpected character at index ${i}`);
    137         }
    138       } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
    139         inQuotes = true;
    140       } else if (end === -1 && tokenChars[code] === 1) {
    141         if (start === -1) start = i;
    142       } else if (start !== -1 && (code === 0x20 || code === 0x09)) {
    143         if (end === -1) end = i;
    144       } else if (code === 0x3b || code === 0x2c) {
    145         if (start === -1) {
    146           throw new SyntaxError(`Unexpected character at index ${i}`);
    147         }
    148 
    149         if (end === -1) end = i;
    150         let value = header.slice(start, end);
    151         if (mustUnescape) {
    152           value = value.replace(/\\/g, '');
    153           mustUnescape = false;
    154         }
    155         push(params, paramName, value);
    156         if (code === 0x2c) {
    157           push(offers, extensionName, params);
    158           params = Object.create(null);
    159           extensionName = undefined;
    160         }
    161 
    162         paramName = undefined;
    163         start = end = -1;
    164       } else {
    165         throw new SyntaxError(`Unexpected character at index ${i}`);
    166       }
    167     }
    168   }
    169 
    170   if (start === -1 || inQuotes) {
    171     throw new SyntaxError('Unexpected end of input');
    172   }
    173 
    174   if (end === -1) end = i;
    175   const token = header.slice(start, end);
    176   if (extensionName === undefined) {
    177     push(offers, token, params);
    178   } else {
    179     if (paramName === undefined) {
    180       push(params, token, true);
    181     } else if (mustUnescape) {
    182       push(params, paramName, token.replace(/\\/g, ''));
    183     } else {
    184       push(params, paramName, token);
    185     }
    186     push(offers, extensionName, params);
    187   }
    188 
    189   return offers;
    190 }
    191 
    192 /**
    193  * Builds the `Sec-WebSocket-Extensions` header field value.
    194  *
    195  * @param {Object} extensions The map of extensions and parameters to format
    196  * @return {String} A string representing the given object
    197  * @public
    198  */
    199 function format(extensions) {
    200   return Object.keys(extensions)
    201     .map((extension) => {
    202       let configurations = extensions[extension];
    203       if (!Array.isArray(configurations)) configurations = [configurations];
    204       return configurations
    205         .map((params) => {
    206           return [extension]
    207             .concat(
    208               Object.keys(params).map((k) => {
    209                 let values = params[k];
    210                 if (!Array.isArray(values)) values = [values];
    211                 return values
    212                   .map((v) => (v === true ? k : `${k}=${v}`))
    213                   .join('; ');
    214               })
    215             )
    216             .join('; ');
    217         })
    218         .join(', ');
    219     })
    220     .join(', ');
    221 }
    222 
    223 module.exports = { format, parse };