UNPKG

6.18 kBJavaScriptView Raw
1'use strict';
2
3const { 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 */
15function 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 */
27function 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 */
179function 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
203module.exports = { format, parse };