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 };