UNPKG

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