extension.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use strict';
  2. const { tokenChars } = require('./validation');
  3. /**
  4. * Adds an offer to the map of extension offers or a parameter to the map of
  5. * parameters.
  6. *
  7. * @param {Object} dest The map of extension offers or parameters
  8. * @param {String} name The extension or parameter name
  9. * @param {(Object|Boolean|String)} elem The extension parameters or the
  10. * parameter value
  11. * @private
  12. */
  13. function push(dest, name, elem) {
  14. if (dest[name] === undefined) dest[name] = [elem];
  15. else dest[name].push(elem);
  16. }
  17. /**
  18. * Parses the `Sec-WebSocket-Extensions` header into an object.
  19. *
  20. * @param {String} header The field value of the header
  21. * @return {Object} The parsed object
  22. * @public
  23. */
  24. function parse(header) {
  25. const offers = Object.create(null);
  26. let params = Object.create(null);
  27. let mustUnescape = false;
  28. let isEscaping = false;
  29. let inQuotes = false;
  30. let extensionName;
  31. let paramName;
  32. let start = -1;
  33. let code = -1;
  34. let end = -1;
  35. let i = 0;
  36. for (; i < header.length; i++) {
  37. code = header.charCodeAt(i);
  38. if (extensionName === undefined) {
  39. if (end === -1 && tokenChars[code] === 1) {
  40. if (start === -1) start = i;
  41. } else if (
  42. i !== 0 &&
  43. (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
  44. ) {
  45. if (end === -1 && start !== -1) end = i;
  46. } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
  47. if (start === -1) {
  48. throw new SyntaxError(`Unexpected character at index ${i}`);
  49. }
  50. if (end === -1) end = i;
  51. const name = header.slice(start, end);
  52. if (code === 0x2c) {
  53. push(offers, name, params);
  54. params = Object.create(null);
  55. } else {
  56. extensionName = name;
  57. }
  58. start = end = -1;
  59. } else {
  60. throw new SyntaxError(`Unexpected character at index ${i}`);
  61. }
  62. } else if (paramName === undefined) {
  63. if (end === -1 && tokenChars[code] === 1) {
  64. if (start === -1) start = i;
  65. } else if (code === 0x20 || code === 0x09) {
  66. if (end === -1 && start !== -1) end = i;
  67. } else if (code === 0x3b || code === 0x2c) {
  68. if (start === -1) {
  69. throw new SyntaxError(`Unexpected character at index ${i}`);
  70. }
  71. if (end === -1) end = i;
  72. push(params, header.slice(start, end), true);
  73. if (code === 0x2c) {
  74. push(offers, extensionName, params);
  75. params = Object.create(null);
  76. extensionName = undefined;
  77. }
  78. start = end = -1;
  79. } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
  80. paramName = header.slice(start, i);
  81. start = end = -1;
  82. } else {
  83. throw new SyntaxError(`Unexpected character at index ${i}`);
  84. }
  85. } else {
  86. //
  87. // The value of a quoted-string after unescaping must conform to the
  88. // token ABNF, so only token characters are valid.
  89. // Ref: https://tools.ietf.org/html/rfc6455#section-9.1
  90. //
  91. if (isEscaping) {
  92. if (tokenChars[code] !== 1) {
  93. throw new SyntaxError(`Unexpected character at index ${i}`);
  94. }
  95. if (start === -1) start = i;
  96. else if (!mustUnescape) mustUnescape = true;
  97. isEscaping = false;
  98. } else if (inQuotes) {
  99. if (tokenChars[code] === 1) {
  100. if (start === -1) start = i;
  101. } else if (code === 0x22 /* '"' */ && start !== -1) {
  102. inQuotes = false;
  103. end = i;
  104. } else if (code === 0x5c /* '\' */) {
  105. isEscaping = true;
  106. } else {
  107. throw new SyntaxError(`Unexpected character at index ${i}`);
  108. }
  109. } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
  110. inQuotes = true;
  111. } else if (end === -1 && tokenChars[code] === 1) {
  112. if (start === -1) start = i;
  113. } else if (start !== -1 && (code === 0x20 || code === 0x09)) {
  114. if (end === -1) end = i;
  115. } else if (code === 0x3b || code === 0x2c) {
  116. if (start === -1) {
  117. throw new SyntaxError(`Unexpected character at index ${i}`);
  118. }
  119. if (end === -1) end = i;
  120. let value = header.slice(start, end);
  121. if (mustUnescape) {
  122. value = value.replace(/\\/g, '');
  123. mustUnescape = false;
  124. }
  125. push(params, paramName, value);
  126. if (code === 0x2c) {
  127. push(offers, extensionName, params);
  128. params = Object.create(null);
  129. extensionName = undefined;
  130. }
  131. paramName = undefined;
  132. start = end = -1;
  133. } else {
  134. throw new SyntaxError(`Unexpected character at index ${i}`);
  135. }
  136. }
  137. }
  138. if (start === -1 || inQuotes || code === 0x20 || code === 0x09) {
  139. throw new SyntaxError('Unexpected end of input');
  140. }
  141. if (end === -1) end = i;
  142. const token = header.slice(start, end);
  143. if (extensionName === undefined) {
  144. push(offers, token, params);
  145. } else {
  146. if (paramName === undefined) {
  147. push(params, token, true);
  148. } else if (mustUnescape) {
  149. push(params, paramName, token.replace(/\\/g, ''));
  150. } else {
  151. push(params, paramName, token);
  152. }
  153. push(offers, extensionName, params);
  154. }
  155. return offers;
  156. }
  157. /**
  158. * Builds the `Sec-WebSocket-Extensions` header field value.
  159. *
  160. * @param {Object} extensions The map of extensions and parameters to format
  161. * @return {String} A string representing the given object
  162. * @public
  163. */
  164. function format(extensions) {
  165. return Object.keys(extensions)
  166. .map((extension) => {
  167. let configurations = extensions[extension];
  168. if (!Array.isArray(configurations)) configurations = [configurations];
  169. return configurations
  170. .map((params) => {
  171. return [extension]
  172. .concat(
  173. Object.keys(params).map((k) => {
  174. let values = params[k];
  175. if (!Array.isArray(values)) values = [values];
  176. return values
  177. .map((v) => (v === true ? k : `${k}=${v}`))
  178. .join('; ');
  179. })
  180. )
  181. .join('; ');
  182. })
  183. .join(', ');
  184. })
  185. .join(', ');
  186. }
  187. module.exports = { format, parse };