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