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