frame-impl.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import { BYTE } from './byte.js';
  2. import { IFrame } from './i-frame.js';
  3. import { StompHeaders } from './stomp-headers.js';
  4. import { IRawFrameType } from './types.js';
  5. /**
  6. * Frame class represents a STOMP frame.
  7. *
  8. * @internal
  9. */
  10. export class FrameImpl implements IFrame {
  11. /**
  12. * STOMP Command
  13. */
  14. public command: string;
  15. /**
  16. * Headers, key value pairs.
  17. */
  18. public headers: StompHeaders;
  19. /**
  20. * Is this frame binary (based on whether body/binaryBody was passed when creating this frame).
  21. */
  22. public isBinaryBody: boolean;
  23. /**
  24. * body of the frame
  25. */
  26. get body(): string {
  27. if (!this._body && this.isBinaryBody) {
  28. this._body = new TextDecoder().decode(this._binaryBody);
  29. }
  30. return this._body || '';
  31. }
  32. private _body: string | undefined;
  33. /**
  34. * body as Uint8Array
  35. */
  36. get binaryBody(): Uint8Array {
  37. if (!this._binaryBody && !this.isBinaryBody) {
  38. this._binaryBody = new TextEncoder().encode(this._body);
  39. }
  40. // At this stage it will definitely have a valid value
  41. return this._binaryBody as Uint8Array;
  42. }
  43. private _binaryBody: Uint8Array | undefined;
  44. private escapeHeaderValues: boolean;
  45. private skipContentLengthHeader: boolean;
  46. /**
  47. * Frame constructor. `command`, `headers` and `body` are available as properties.
  48. *
  49. * @internal
  50. */
  51. constructor(params: {
  52. command: string;
  53. headers?: StompHeaders;
  54. body?: string;
  55. binaryBody?: Uint8Array;
  56. escapeHeaderValues?: boolean;
  57. skipContentLengthHeader?: boolean;
  58. }) {
  59. const {
  60. command,
  61. headers,
  62. body,
  63. binaryBody,
  64. escapeHeaderValues,
  65. skipContentLengthHeader,
  66. } = params;
  67. this.command = command;
  68. this.headers = (Object as any).assign({}, headers || {});
  69. if (binaryBody) {
  70. this._binaryBody = binaryBody;
  71. this.isBinaryBody = true;
  72. } else {
  73. this._body = body || '';
  74. this.isBinaryBody = false;
  75. }
  76. this.escapeHeaderValues = escapeHeaderValues || false;
  77. this.skipContentLengthHeader = skipContentLengthHeader || false;
  78. }
  79. /**
  80. * deserialize a STOMP Frame from raw data.
  81. *
  82. * @internal
  83. */
  84. public static fromRawFrame(
  85. rawFrame: IRawFrameType,
  86. escapeHeaderValues: boolean
  87. ): FrameImpl {
  88. const headers: StompHeaders = {};
  89. const trim = (str: string): string => str.replace(/^\s+|\s+$/g, '');
  90. // In case of repeated headers, as per standards, first value need to be used
  91. for (const header of rawFrame.headers.reverse()) {
  92. const idx = header.indexOf(':');
  93. const key = trim(header[0]);
  94. let value = trim(header[1]);
  95. if (
  96. escapeHeaderValues &&
  97. rawFrame.command !== 'CONNECT' &&
  98. rawFrame.command !== 'CONNECTED'
  99. ) {
  100. value = FrameImpl.hdrValueUnEscape(value);
  101. }
  102. headers[key] = value;
  103. }
  104. return new FrameImpl({
  105. command: rawFrame.command as string,
  106. headers,
  107. binaryBody: rawFrame.binaryBody,
  108. escapeHeaderValues,
  109. });
  110. }
  111. /**
  112. * @internal
  113. */
  114. public toString(): string {
  115. return this.serializeCmdAndHeaders();
  116. }
  117. /**
  118. * serialize this Frame in a format suitable to be passed to WebSocket.
  119. * If the body is string the output will be string.
  120. * If the body is binary (i.e. of type Unit8Array) it will be serialized to ArrayBuffer.
  121. *
  122. * @internal
  123. */
  124. public serialize(): string | ArrayBuffer {
  125. const cmdAndHeaders = this.serializeCmdAndHeaders();
  126. if (this.isBinaryBody) {
  127. return FrameImpl.toUnit8Array(
  128. cmdAndHeaders,
  129. this._binaryBody as Uint8Array
  130. ).buffer;
  131. } else {
  132. return cmdAndHeaders + this._body + BYTE.NULL;
  133. }
  134. }
  135. private serializeCmdAndHeaders(): string {
  136. const lines = [this.command];
  137. if (this.skipContentLengthHeader) {
  138. delete this.headers['content-length'];
  139. }
  140. for (const name of Object.keys(this.headers || {})) {
  141. const value = this.headers[name];
  142. if (
  143. this.escapeHeaderValues &&
  144. this.command !== 'CONNECT' &&
  145. this.command !== 'CONNECTED'
  146. ) {
  147. lines.push(`${name}:${FrameImpl.hdrValueEscape(`${value}`)}`);
  148. } else {
  149. lines.push(`${name}:${value}`);
  150. }
  151. }
  152. if (
  153. this.isBinaryBody ||
  154. (!this.isBodyEmpty() && !this.skipContentLengthHeader)
  155. ) {
  156. lines.push(`content-length:${this.bodyLength()}`);
  157. }
  158. return lines.join(BYTE.LF) + BYTE.LF + BYTE.LF;
  159. }
  160. private isBodyEmpty(): boolean {
  161. return this.bodyLength() === 0;
  162. }
  163. private bodyLength(): number {
  164. const binaryBody = this.binaryBody;
  165. return binaryBody ? binaryBody.length : 0;
  166. }
  167. /**
  168. * Compute the size of a UTF-8 string by counting its number of bytes
  169. * (and not the number of characters composing the string)
  170. */
  171. private static sizeOfUTF8(s: string): number {
  172. return s ? new TextEncoder().encode(s).length : 0;
  173. }
  174. private static toUnit8Array(
  175. cmdAndHeaders: string,
  176. binaryBody: Uint8Array
  177. ): Uint8Array {
  178. const uint8CmdAndHeaders = new TextEncoder().encode(cmdAndHeaders);
  179. const nullTerminator = new Uint8Array([0]);
  180. const uint8Frame = new Uint8Array(
  181. uint8CmdAndHeaders.length + binaryBody.length + nullTerminator.length
  182. );
  183. uint8Frame.set(uint8CmdAndHeaders);
  184. uint8Frame.set(binaryBody, uint8CmdAndHeaders.length);
  185. uint8Frame.set(
  186. nullTerminator,
  187. uint8CmdAndHeaders.length + binaryBody.length
  188. );
  189. return uint8Frame;
  190. }
  191. /**
  192. * Serialize a STOMP frame as per STOMP standards, suitable to be sent to the STOMP broker.
  193. *
  194. * @internal
  195. */
  196. public static marshall(params: {
  197. command: string;
  198. headers?: StompHeaders;
  199. body?: string;
  200. binaryBody?: Uint8Array;
  201. escapeHeaderValues?: boolean;
  202. skipContentLengthHeader?: boolean;
  203. }) {
  204. const frame = new FrameImpl(params);
  205. return frame.serialize();
  206. }
  207. /**
  208. * Escape header values
  209. */
  210. private static hdrValueEscape(str: string): string {
  211. return str
  212. .replace(/\\/g, '\\\\')
  213. .replace(/\r/g, '\\r')
  214. .replace(/\n/g, '\\n')
  215. .replace(/:/g, '\\c');
  216. }
  217. /**
  218. * UnEscape header values
  219. */
  220. private static hdrValueUnEscape(str: string): string {
  221. return str
  222. .replace(/\\r/g, '\r')
  223. .replace(/\\n/g, '\n')
  224. .replace(/\\c/g, ':')
  225. .replace(/\\\\/g, '\\');
  226. }
  227. }