amqplib 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/lib/codec.js +24 -0
- package/lib/connect.js +8 -0
- package/lib/connection.js +25 -1
- package/lib/frame.js +5 -1
- package/lib/properties.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Change log for amqplib
|
|
2
2
|
|
|
3
|
+
## v2.3.1
|
|
4
|
+
- Throw a clear `RangeError` from `publish` when an encoded headers or `arguments` table exceeds the 64 KiB encoding buffer. Previously the outcome depended on where the boundary fell: a string or Buffer value crossing the end of the buffer was silently truncated, producing a frame whose table length prefix did not match its contents, which RabbitMQ rejected by closing the connection with a 541 `INTERNAL_ERROR` after `publish` had returned; at other positions a bare `offset is out of range` error escaped from `publish` instead (fixes #867)
|
|
5
|
+
|
|
6
|
+
## v2.3.0
|
|
7
|
+
- Reject a `connection.tune` whose `frameMax` is below the protocol minimum of 4096 (the spec allows 0 or at least frame-min-size). Previously such a value was accepted, and the first publish whose encoded frames total 2048 bytes or more entered a loop in `sendContent` that never advanced, exhausting memory. `sendContent` now also throws if `frameMax` leaves no room for a body frame (GHSA-37f9-7m79-6866, reported by Rizwan Bedekar ([@r-bedekar](https://github.com/r-bedekar)); fix based in part on their patch and tests)
|
|
8
|
+
- Add a `maxFrameSize` connection option, also settable as a URL query parameter, bounding the payload size of frames accepted from the server. The default is 64 MiB, or the negotiated `frameMax` if that is larger, so raising `frameMax` needs no second option; `0` disables the limit. Incoming frame sizes were previously unchecked, so a frame header declaring up to 4 GiB made the client buffer, and repeatedly re-copy, that much data before looking at it. An oversized frame is now refused from its header alone and closes the connection with `Frame size N exceeds maxFrameSize M`. The limit is deliberately separate from the negotiated `frameMax`, since some brokers send larger frames without splitting them (see #784). **Upgrade note:** a broker that delivers an unsplit frame larger than the limit will now have that connection closed; pass `maxFrameSize: 0`, or a higher value, to restore the previous behaviour (GHSA-37f9-7m79-6866)
|
|
9
|
+
|
|
3
10
|
## v2.2.1
|
|
4
11
|
- Return the server acknowledgement from promise-based `channel.cancel`, matching the callback API (thanks [@edsonsims](https://github.com/edsonsims)).
|
|
5
12
|
- Type `cancel` replies as `Replies.Cancel` (`{ consumerTag }`) in both the promise and callback API declarations, which previously claimed `Replies.Empty`.
|
package/lib/codec.js
CHANGED
|
@@ -62,12 +62,20 @@ function isFloatingPoint(n) {
|
|
|
62
62
|
return n >= 0x8000000000000000 || (Math.abs(n) < 0x4000000000000 && Math.floor(n) !== n);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function checkSpace(buffer, offset, size) {
|
|
66
|
+
if (offset + size > buffer.length) {
|
|
67
|
+
throw new RangeError(`Encoded field table (headers or arguments) exceeds the ${buffer.length} byte encoding buffer`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
function encodeTable(buffer, val, offset) {
|
|
66
72
|
const start = offset;
|
|
73
|
+
checkSpace(buffer, offset, 4);
|
|
67
74
|
offset += 4; // leave room for the table length
|
|
68
75
|
for (const key in val) {
|
|
69
76
|
if (val[key] !== undefined) {
|
|
70
77
|
const len = Buffer.byteLength(key);
|
|
78
|
+
checkSpace(buffer, offset, 1 + len);
|
|
71
79
|
buffer.writeUInt8(len, offset);
|
|
72
80
|
offset++;
|
|
73
81
|
buffer.write(key, offset, 'utf8');
|
|
@@ -82,6 +90,7 @@ function encodeTable(buffer, val, offset) {
|
|
|
82
90
|
|
|
83
91
|
function encodeArray(buffer, val, offset) {
|
|
84
92
|
const start = offset;
|
|
93
|
+
checkSpace(buffer, offset, 4);
|
|
85
94
|
offset += 4;
|
|
86
95
|
for (let i = 0, num = val.length; i < num; i++) {
|
|
87
96
|
offset += encodeFieldValue(buffer, val[i], offset);
|
|
@@ -133,6 +142,7 @@ function encodeFieldValue(buffer, value, offset) {
|
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
function tag(t) {
|
|
145
|
+
checkSpace(buffer, offset, 1);
|
|
136
146
|
buffer.write(t, offset);
|
|
137
147
|
offset++;
|
|
138
148
|
}
|
|
@@ -142,6 +152,7 @@ function encodeFieldValue(buffer, value, offset) {
|
|
|
142
152
|
// no shortstr in field tables
|
|
143
153
|
const len = Buffer.byteLength(val, 'utf8');
|
|
144
154
|
tag('S');
|
|
155
|
+
checkSpace(buffer, offset, 4 + len);
|
|
145
156
|
buffer.writeUInt32BE(len, offset);
|
|
146
157
|
offset += 4;
|
|
147
158
|
buffer.write(val, offset, 'utf8');
|
|
@@ -156,6 +167,7 @@ function encodeFieldValue(buffer, value, offset) {
|
|
|
156
167
|
offset += encodeArray(buffer, val, offset);
|
|
157
168
|
} else if (Buffer.isBuffer(val)) {
|
|
158
169
|
tag('x');
|
|
170
|
+
checkSpace(buffer, offset, 4 + val.length);
|
|
159
171
|
buffer.writeUInt32BE(val.length, offset);
|
|
160
172
|
offset += 4;
|
|
161
173
|
val.copy(buffer, offset);
|
|
@@ -167,6 +179,7 @@ function encodeFieldValue(buffer, value, offset) {
|
|
|
167
179
|
break;
|
|
168
180
|
case 'boolean':
|
|
169
181
|
tag('t');
|
|
182
|
+
checkSpace(buffer, offset, 1);
|
|
170
183
|
buffer.writeUInt8(val ? 1 : 0, offset);
|
|
171
184
|
offset++;
|
|
172
185
|
break;
|
|
@@ -175,48 +188,56 @@ function encodeFieldValue(buffer, value, offset) {
|
|
|
175
188
|
case 'double':
|
|
176
189
|
case 'float64':
|
|
177
190
|
tag('d');
|
|
191
|
+
checkSpace(buffer, offset, 8);
|
|
178
192
|
buffer.writeDoubleBE(val, offset);
|
|
179
193
|
offset += 8;
|
|
180
194
|
break;
|
|
181
195
|
case 'byte':
|
|
182
196
|
case 'int8':
|
|
183
197
|
tag('b');
|
|
198
|
+
checkSpace(buffer, offset, 1);
|
|
184
199
|
buffer.writeInt8(val, offset);
|
|
185
200
|
offset++;
|
|
186
201
|
break;
|
|
187
202
|
case 'unsignedbyte':
|
|
188
203
|
case 'uint8':
|
|
189
204
|
tag('B');
|
|
205
|
+
checkSpace(buffer, offset, 1);
|
|
190
206
|
buffer.writeUInt8(val, offset);
|
|
191
207
|
offset++;
|
|
192
208
|
break;
|
|
193
209
|
case 'short':
|
|
194
210
|
case 'int16':
|
|
195
211
|
tag('s');
|
|
212
|
+
checkSpace(buffer, offset, 2);
|
|
196
213
|
buffer.writeInt16BE(val, offset);
|
|
197
214
|
offset += 2;
|
|
198
215
|
break;
|
|
199
216
|
case 'unsignedshort':
|
|
200
217
|
case 'uint16':
|
|
201
218
|
tag('u');
|
|
219
|
+
checkSpace(buffer, offset, 2);
|
|
202
220
|
buffer.writeUInt16BE(val, offset);
|
|
203
221
|
offset += 2;
|
|
204
222
|
break;
|
|
205
223
|
case 'int':
|
|
206
224
|
case 'int32':
|
|
207
225
|
tag('I');
|
|
226
|
+
checkSpace(buffer, offset, 4);
|
|
208
227
|
buffer.writeInt32BE(val, offset);
|
|
209
228
|
offset += 4;
|
|
210
229
|
break;
|
|
211
230
|
case 'unsignedint':
|
|
212
231
|
case 'uint32':
|
|
213
232
|
tag('i');
|
|
233
|
+
checkSpace(buffer, offset, 4);
|
|
214
234
|
buffer.writeUInt32BE(val, offset);
|
|
215
235
|
offset += 4;
|
|
216
236
|
break;
|
|
217
237
|
case 'long':
|
|
218
238
|
case 'int64':
|
|
219
239
|
tag('l');
|
|
240
|
+
checkSpace(buffer, offset, 8);
|
|
220
241
|
buffer.writeBigInt64BE(BigInt(val), offset);
|
|
221
242
|
offset += 8;
|
|
222
243
|
break;
|
|
@@ -225,17 +246,20 @@ function encodeFieldValue(buffer, value, offset) {
|
|
|
225
246
|
// `{'!': type, value: val}
|
|
226
247
|
case 'timestamp':
|
|
227
248
|
tag('T');
|
|
249
|
+
checkSpace(buffer, offset, 8);
|
|
228
250
|
buffer.writeBigUInt64BE(BigInt(val), offset);
|
|
229
251
|
offset += 8;
|
|
230
252
|
break;
|
|
231
253
|
case 'float':
|
|
232
254
|
tag('f');
|
|
255
|
+
checkSpace(buffer, offset, 4);
|
|
233
256
|
buffer.writeFloatBE(val, offset);
|
|
234
257
|
offset += 4;
|
|
235
258
|
break;
|
|
236
259
|
case 'decimal':
|
|
237
260
|
tag('D');
|
|
238
261
|
if (Object.hasOwn(val, 'places') && Object.hasOwn(val, 'digits') && val.places >= 0 && val.places < 256) {
|
|
262
|
+
checkSpace(buffer, offset, 5);
|
|
239
263
|
buffer[offset] = val.places;
|
|
240
264
|
offset++;
|
|
241
265
|
buffer.writeUInt32BE(val.digits, offset);
|
package/lib/connect.js
CHANGED
|
@@ -47,6 +47,11 @@ function openFrames(vhost, query, credentials, extraClientProperties) {
|
|
|
47
47
|
return val === undefined ? def : parseInt(val, 10);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function maxFrameSizeOrUndefined(val) {
|
|
51
|
+
if (val == null) return undefined;
|
|
52
|
+
return /^\d+$/.test(String(val)) ? Number(val) : NaN;
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
const clientProperties = Object.create(CLIENT_PROPERTIES);
|
|
51
56
|
|
|
52
57
|
return {
|
|
@@ -61,6 +66,8 @@ function openFrames(vhost, query, credentials, extraClientProperties) {
|
|
|
61
66
|
frameMax: intOrDefault(query.frameMax, 131072),
|
|
62
67
|
heartbeat: query.heartbeat != null ? intOrDefault(query.heartbeat, 0) : null,
|
|
63
68
|
|
|
69
|
+
maxFrameSize: maxFrameSizeOrUndefined(query.maxFrameSize),
|
|
70
|
+
|
|
64
71
|
// open
|
|
65
72
|
virtualHost: vhost,
|
|
66
73
|
capabilities: '',
|
|
@@ -120,6 +127,7 @@ function connect(url, socketOptions, openCallback) {
|
|
|
120
127
|
channelMax: url.channelMax,
|
|
121
128
|
frameMax: url.frameMax,
|
|
122
129
|
heartbeat: url.heartbeat,
|
|
130
|
+
maxFrameSize: url.maxFrameSize,
|
|
123
131
|
};
|
|
124
132
|
|
|
125
133
|
fields = openFrames(url.vhost, config, sockopts.credentials || credentials.plain(user, pass), extraClientProperties);
|
package/lib/connection.js
CHANGED
|
@@ -27,6 +27,7 @@ const DEFAULT_WRITE_HWM = 1024;
|
|
|
27
27
|
// at once. Note that this is less than the minimum frame size: if it
|
|
28
28
|
// was greater, we might have to fragment the content.
|
|
29
29
|
const SINGLE_CHUNK_THRESHOLD = 2048;
|
|
30
|
+
const DEFAULT_MAX_FRAME_SIZE = 64 * 1024 * 1024;
|
|
30
31
|
|
|
31
32
|
class Connection extends EventEmitter {
|
|
32
33
|
constructor(underlying) {
|
|
@@ -38,6 +39,7 @@ class Connection extends EventEmitter {
|
|
|
38
39
|
// frames
|
|
39
40
|
this.rest = Buffer.alloc(0);
|
|
40
41
|
this.frameMax = constants.FRAME_MIN_SIZE;
|
|
42
|
+
this.maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
|
|
41
43
|
this.sentSinceLastCheck = false;
|
|
42
44
|
this.recvSinceLastCheck = false;
|
|
43
45
|
|
|
@@ -86,6 +88,17 @@ class Connection extends EventEmitter {
|
|
|
86
88
|
const self = this;
|
|
87
89
|
const openCallback = openCallback0 || (() => {});
|
|
88
90
|
|
|
91
|
+
const maxFrameSize = allFields.maxFrameSize;
|
|
92
|
+
const maxFrameSizeDefaulted = maxFrameSize === undefined || maxFrameSize === null;
|
|
93
|
+
if (maxFrameSizeDefaulted) {
|
|
94
|
+
self.maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
|
|
95
|
+
} else if (!Number.isInteger(maxFrameSize) || (maxFrameSize !== 0 && maxFrameSize < constants.FRAME_MIN_SIZE)) {
|
|
96
|
+
bail(new TypeError(fmt('maxFrameSize must be 0 (no limit) or an integer of at least %d; got %s', constants.FRAME_MIN_SIZE, maxFrameSize)));
|
|
97
|
+
return;
|
|
98
|
+
} else {
|
|
99
|
+
self.maxFrameSize = maxFrameSize;
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
// This is where we'll put our negotiated values
|
|
90
103
|
const tunedOptions = Object.create(allFields);
|
|
91
104
|
|
|
@@ -157,6 +170,10 @@ class Connection extends EventEmitter {
|
|
|
157
170
|
break;
|
|
158
171
|
case defs.ConnectionTune: {
|
|
159
172
|
const fields = reply.fields;
|
|
173
|
+
if (fields.frameMax !== 0 && fields.frameMax < constants.FRAME_MIN_SIZE) {
|
|
174
|
+
bail(new Error(fmt('Server frameMax %d is below the protocol minimum %d', fields.frameMax, constants.FRAME_MIN_SIZE)));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
160
177
|
tunedOptions.frameMax = negotiate(fields.frameMax, allFields.frameMax);
|
|
161
178
|
tunedOptions.channelMax = negotiate(fields.channelMax, allFields.channelMax);
|
|
162
179
|
if (allFields.heartbeat == null) {
|
|
@@ -191,6 +208,9 @@ class Connection extends EventEmitter {
|
|
|
191
208
|
// value is zero, meaning "no, no limits"
|
|
192
209
|
self.channelMax = tunedOptions.channelMax || 0xffff;
|
|
193
210
|
self.frameMax = tunedOptions.frameMax || 0xffffffff;
|
|
211
|
+
if (maxFrameSizeDefaulted && tunedOptions.frameMax !== 0 && tunedOptions.frameMax > self.maxFrameSize) {
|
|
212
|
+
self.maxFrameSize = tunedOptions.frameMax;
|
|
213
|
+
}
|
|
194
214
|
// 0 means "no heartbeat", rather than "maximum period of
|
|
195
215
|
// heartbeating"
|
|
196
216
|
self.heartbeat = tunedOptions.heartbeat;
|
|
@@ -526,6 +546,9 @@ class Connection extends EventEmitter {
|
|
|
526
546
|
const buffer = this.channels[channel].buffer;
|
|
527
547
|
|
|
528
548
|
const maxBody = this.frameMax - FRAME_OVERHEAD;
|
|
549
|
+
if (!(maxBody > 0)) {
|
|
550
|
+
throw new Error(fmt('Cannot send content: frameMax %d leaves no room for a body frame', this.frameMax));
|
|
551
|
+
}
|
|
529
552
|
|
|
530
553
|
for (let offset = 0; offset < body.length; offset += maxBody) {
|
|
531
554
|
const end = offset + maxBody;
|
|
@@ -539,7 +562,7 @@ class Connection extends EventEmitter {
|
|
|
539
562
|
|
|
540
563
|
recvFrame() {
|
|
541
564
|
// %%% identifying invariants might help here?
|
|
542
|
-
const frame = parseFrame(this.rest);
|
|
565
|
+
const frame = parseFrame(this.rest, this.maxFrameSize);
|
|
543
566
|
|
|
544
567
|
if (!frame) {
|
|
545
568
|
const incoming = this.stream.read();
|
|
@@ -654,3 +677,4 @@ function isFatalError(error) {
|
|
|
654
677
|
|
|
655
678
|
module.exports.Connection = Connection;
|
|
656
679
|
module.exports.isFatalError = isFatalError;
|
|
680
|
+
module.exports.DEFAULT_MAX_FRAME_SIZE = DEFAULT_MAX_FRAME_SIZE;
|
package/lib/frame.js
CHANGED
|
@@ -78,7 +78,7 @@ module.exports.makeBodyFrame = (channel, payload) => {
|
|
|
78
78
|
* @arg { number } max
|
|
79
79
|
* @returns { FrameStructure | boolean }
|
|
80
80
|
*/
|
|
81
|
-
function parseFrame(bin) {
|
|
81
|
+
function parseFrame(bin, max) {
|
|
82
82
|
if (bin.length < FRAME_HEADER_BYTES) {
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
@@ -87,6 +87,10 @@ function parseFrame(bin) {
|
|
|
87
87
|
const channel = bin.readUInt16BE(1);
|
|
88
88
|
const size = bin.readUInt32BE(3);
|
|
89
89
|
|
|
90
|
+
if (max > 0 && size > max) {
|
|
91
|
+
throw new Error(`Frame size ${size} exceeds maxFrameSize ${max}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
90
94
|
const totalSize = FRAME_HEADER_BYTES + size + FRAME_END_BYTES;
|
|
91
95
|
|
|
92
96
|
if (bin.length < totalSize) {
|
package/lib/properties.d.ts
CHANGED