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