1 | import { BYTE } from './byte';
|
2 | import { IFrame } from './i-frame';
|
3 | import { StompHeaders } from './stomp-headers';
|
4 | import { IRawFrameType } from './types';
|
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;
|
37 |
|
38 | |
39 |
|
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 |
|
54 |
|
55 |
|
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 |
|
89 |
|
90 |
|
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 |
|
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 |
|
127 |
|
128 | public toString(): string {
|
129 | return this.serializeCmdAndHeaders();
|
130 | }
|
131 |
|
132 | |
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
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 |