@aunucore/baileys 1.0.75 โ 1.0.77
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/README.md +1 -1
- package/lib/Socket/graphql.js +9 -2
- package/lib/Socket/messages-recv.js +13 -0
- package/lib/Socket/messages-send.js +17 -6
- package/lib/Socket/socket.js +140 -16
- package/lib/Utils/generics.js +11 -1
- package/lib/Utils/make-mutex.js +9 -9
- package/lib/Utils/messages-media.js +62 -3
- package/lib/Utils/noise-handler.js +43 -11
- package/lib/Utils/session-watchdog.js +23 -2
- package/lib/Utils/use-multi-file-auth-state.js +8 -1
- package/lib/WABinary/decode.js +29 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,9 +128,9 @@ Everything else is the upstream core. The patch layer stays small and deliberate
|
|
|
128
128
|
|
|
129
129
|
### ๐ฐ Recent releases
|
|
130
130
|
|
|
131
|
+
- **1.0.76 โ Hunt round 3: the decoder + trust-boundary batch.** Every claim in an external deep-audit was verified against the tree (with repros) before anything was patched. The WABinary decoder gets a 512-level nesting cap (a ~100 KB stanza previously recursed 20,000 deep into a raw stack-overflow RangeError) and a 128 MiB zlib output cap (a max-size frame could inflate ~1000ร toward OOM). Media downloads now verify the WhatsApp HMAC on full downloads โ tampered or truncated media errors out instead of decrypting silently โ and download hosts are allowlisted to WhatsApp/Facebook CDN domains, closing a sender-driven SSRF (foreign hosts fall back to `mmg.whatsapp.net`). Auth files are written `0600` (they hold private keys; old `0644` files self-heal on next save), GraphQL error bodies are drained instead of pinning keep-alive sockets, `link_code_companion_reg` key material is length-validated before any pairing secret is derived, the session watchdog also hears raw WebSocket frames (buffered-event delays no longer read as a false deaf session), and a dead `taskTimeout` in the mutex factory is removed. Deliberately untouched, with recorded reasoning: `isTrustedIdentity` (upstream TOFU), unbounded host store arrays, and the app-state transaction design.
|
|
131
132
|
- **1.0.75 โ Partial-config safety + watchdog cleanup.** A partial `transactionOpts` in the socket config can no longer kill every key-store transaction: the config is deep-merged over the defaults and the transaction layer falls back per-field (`maxCommitRetries` 10, `delayBetweenTriesMs` 3000, retries clamped to at least 1 so an explicit `0` means exactly one attempt) โ previously the shallow config spread replaced the whole default object, `maxCommitRetries` came out `undefined`, and every transaction threw `failed to commit transaction mutations` without a single commit attempt. The session watchdog's interval is now stopped when the socket ends (its `stop()` existed but was never called anywhere), closing a one-timer-and-socket-graph leak per reconnect cycle.
|
|
132
133
|
- **1.0.74 โ Fix-all batch.** The entire 1.0.74 heartbeat queue, closed in one version: `resyncAppState` can no longer spin forever holding the key-store transaction mutex (server-omitted collections finish with a warning, plus a 25-round cap), `startByte`-only media downloads no longer silently return an empty stream, an all-failed sticker pack throws Boom 400 instead of uploading empty, key-material guards now check TYPES (Buffer/string only) at six sites so wrong-typed creds fail descriptively instead of as raw TypeErrors, the noise handshake guards a missing `noiseKey` up front, the pre-login `creds.me` dereference family is closed (acks for `<unavailable>` stanzas survive fresh pairing), and two per-entity maps (`RateLimiter.hits`, bug-guard `recentBySender`) evict stale keys with a lazy sweep.
|
|
133
|
-
- **1.0.73 โ Heartbeat queue + a new README.** Three verified heartbeat fixes: a corrupt frame header can no longer wedge `decodeFrame` (zero-length headers are skipped, frames over 10 MiB fail loudly instead of buffering forever), `botRecentSecretsByChat` is capped at 1,000 chats FIFO, and bare key-store writes are serialized through the transaction mutex so a `migrateSession` commit can't overwrite a fresher session record. Plus a fully rewritten README and 46 curated npm keywords.
|
|
134
134
|
Full changelog for every version since 1.0.2: **[docs](https://docsjs.aunucore.eu.cc)** ยท **[GitHub Releases](https://github.com/AunuCore/AunuBaileys-lib/releases)**.
|
|
135
135
|
|
|
136
136
|
### ๐ Credits
|
package/lib/Socket/graphql.js
CHANGED
|
@@ -68,7 +68,12 @@ export const executeWWWGraphQL = async (docId, variables, accessToken, dataPath
|
|
|
68
68
|
body
|
|
69
69
|
});
|
|
70
70
|
if (!res.ok) {
|
|
71
|
-
|
|
71
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ drain the error body before
|
|
72
|
+
// throwing. An unconsumed fetch body pins its keep-alive socket
|
|
73
|
+
// out of the undici pool until GC; repeated GraphQL errors would
|
|
74
|
+
// steadily starve the pool. The body text also improves the error.
|
|
75
|
+
const errBody = await res.text().catch(() => '');
|
|
76
|
+
throw new Boom(`GraphQL HTTP error ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ''}`, { statusCode: res.status });
|
|
72
77
|
}
|
|
73
78
|
const json = await res.json();
|
|
74
79
|
return parseGraphQLResponse(json, dataPath);
|
|
@@ -104,7 +109,9 @@ export const executeWamoGraphQL = async (docId, variables, wamoAuth = {}, dataPa
|
|
|
104
109
|
body: params.toString()
|
|
105
110
|
});
|
|
106
111
|
if (!res.ok) {
|
|
107
|
-
|
|
112
|
+
// AunuBaileys (1.0.76): drain the error body (see executeWWWGraphQL).
|
|
113
|
+
const errBody = await res.text().catch(() => '');
|
|
114
|
+
throw new Boom(`Wamo GraphQL HTTP error ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ''}`, { statusCode: res.status });
|
|
108
115
|
}
|
|
109
116
|
const json = await res.json();
|
|
110
117
|
return parseGraphQLResponse(json, dataPath);
|
|
@@ -523,6 +523,19 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
523
523
|
const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'));
|
|
524
524
|
const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'));
|
|
525
525
|
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'));
|
|
526
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ toRequiredBuffer only
|
|
527
|
+
// checks PRESENCE. The wrapped ephemeral key has a fixed
|
|
528
|
+
// wire shape (32B salt + 16B IV + 32B encrypted pubkey =
|
|
529
|
+
// 80) and an identity pubkey is 32B raw / 33B prefixed;
|
|
530
|
+
// anything else is a malformed stanza. Most bad lengths
|
|
531
|
+
// already died inside libsignal BEFORE advSecretKey was
|
|
532
|
+
// assigned, but 31/32-byte inputs slipped through and
|
|
533
|
+
// derived a garbage advSecretKey that then got persisted.
|
|
534
|
+
// Refuse up front instead of deriving creds from junk.
|
|
535
|
+
if (primaryEphemeralPublicKeyWrapped.length < 80 ||
|
|
536
|
+
(primaryIdentityPublicKey.length !== 32 && primaryIdentityPublicKey.length !== 33)) {
|
|
537
|
+
throw new Boom('link_code_companion_reg: malformed key material in server stanza โ refusing to derive pairing secrets from it', { statusCode: 400 });
|
|
538
|
+
}
|
|
526
539
|
const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped);
|
|
527
540
|
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
|
|
528
541
|
const random = randomBytes(32);
|
|
@@ -334,12 +334,23 @@ export const makeMessagesSocket = (config) => {
|
|
|
334
334
|
// the transport to reopen, bounded by connectTimeoutMs; a connection
|
|
335
335
|
// that closes for good still fails fast via waitForSocketOpen itself.
|
|
336
336
|
const waitMs = config.connectTimeoutMs ?? 60000;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
337
|
+
const openWait = waitForSocketOpen();
|
|
338
|
+
try {
|
|
339
|
+
await Promise.race([
|
|
340
|
+
openWait,
|
|
341
|
+
delay(waitMs).then(() => {
|
|
342
|
+
throw new Boom(`Timed out waiting ${waitMs}ms for the connection to open before sending`, { statusCode: 408 }); // 408 = DisconnectReason.timedOut
|
|
343
|
+
})
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
// AunuBaileys (1.0.77, HB-167): when the timeout wins,
|
|
348
|
+
// the socket-open waiter is still pending โ detach its
|
|
349
|
+
// ws listeners now instead of leaving three behind per
|
|
350
|
+
// failed send until the socket settles on its own.
|
|
351
|
+
openWait.cancel?.();
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
343
354
|
}
|
|
344
355
|
// AunuBaileys (1.0.68): fail with a clear Boom instead of a raw
|
|
345
356
|
// TypeError when sending before pairing completes or with a
|
package/lib/Socket/socket.js
CHANGED
|
@@ -92,6 +92,15 @@ export const makeSocket = (config) => {
|
|
|
92
92
|
let epoch = 1;
|
|
93
93
|
let keepAliveReq;
|
|
94
94
|
let qrTimer;
|
|
95
|
+
// AunuBaileys (1.0.77, HB-243): true while a pair-device QR chain
|
|
96
|
+
// is cycling โ a duplicate pair-device IQ must not start a second
|
|
97
|
+
// chain on the shared qrTimer slot.
|
|
98
|
+
let qrChainActive = false;
|
|
99
|
+
// AunuBaileys (1.0.77, HB-213): resolved when the Noise handshake
|
|
100
|
+
// completes (finishInit in validateConnection), so the pairing
|
|
101
|
+
// request can wait for an encrypted channel.
|
|
102
|
+
let resolveHandshakeComplete;
|
|
103
|
+
const handshakeComplete = new Promise((resolve) => { resolveHandshakeComplete = resolve; });
|
|
95
104
|
let closed = false;
|
|
96
105
|
let lastDisconnectError;
|
|
97
106
|
const uqTagId = generateMdTagPrefix();
|
|
@@ -226,6 +235,7 @@ export const makeSocket = (config) => {
|
|
|
226
235
|
}
|
|
227
236
|
}).finish());
|
|
228
237
|
noise.finishInit();
|
|
238
|
+
resolveHandshakeComplete?.();
|
|
229
239
|
startKeepAliveRequest();
|
|
230
240
|
};
|
|
231
241
|
const getAvailablePreKeysOnServer = async () => {
|
|
@@ -278,12 +288,17 @@ export const makeSocket = (config) => {
|
|
|
278
288
|
await uploadPreKeys();
|
|
279
289
|
}
|
|
280
290
|
};
|
|
291
|
+
// AunuBaileys (1.0.77, HB-156): decodeFrame invocations must never
|
|
292
|
+
// overlap โ the noise handler slices ONE shared inBytes buffer, so
|
|
293
|
+
// two concurrent calls interleave and deliver stanzas out of wire
|
|
294
|
+
// order. Every WS message is chained through a single queue.
|
|
295
|
+
let frameDecodeChain = Promise.resolve();
|
|
281
296
|
const onMessageReceived = (data) => {
|
|
282
297
|
// AunuBaileys (1.0.67): decodeFrame is async โ a malformed frame
|
|
283
298
|
// (bad GCM tag, truncated binary node) or a throwing frame
|
|
284
299
|
// listener must not become an unhandled rejection that kills
|
|
285
300
|
// the process; route failures to onUnexpectedError instead.
|
|
286
|
-
|
|
301
|
+
frameDecodeChain = frameDecodeChain.then(() => noise.decodeFrame(data, frame => {
|
|
287
302
|
// reset ping timeout
|
|
288
303
|
lastDateRecv = new Date();
|
|
289
304
|
let anyTriggered = false;
|
|
@@ -311,7 +326,20 @@ export const makeSocket = (config) => {
|
|
|
311
326
|
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
|
|
312
327
|
}
|
|
313
328
|
}
|
|
314
|
-
})).catch(err =>
|
|
329
|
+
})).catch(err => {
|
|
330
|
+
// AunuBaileys (1.0.77, HB-185/HB-207): a frame-layer failure
|
|
331
|
+
// (oversize header, GCM decrypt failure) leaves the noise
|
|
332
|
+
// stream unrecoverable โ the header is never consumed, or
|
|
333
|
+
// the read counter desyncs so every later frame fails too.
|
|
334
|
+
// End the connection so the host reconnects at once instead
|
|
335
|
+
// of wedging until the keep-alive watchdog notices (~65s).
|
|
336
|
+
if (err?.isFrameFatal) {
|
|
337
|
+
end(err);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
onUnexpectedError(err, 'onMessageReceived');
|
|
341
|
+
}
|
|
342
|
+
});
|
|
315
343
|
};
|
|
316
344
|
// AunuBaileys: socket end handlers (pola v7) + fetch Mex (dari @itsliaaa/baileys)
|
|
317
345
|
const socketEndHandlers = [];
|
|
@@ -329,6 +357,7 @@ export const makeSocket = (config) => {
|
|
|
329
357
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
330
358
|
clearInterval(keepAliveReq);
|
|
331
359
|
clearTimeout(qrTimer);
|
|
360
|
+
qrChainActive = false;
|
|
332
361
|
ws.removeAllListeners('close');
|
|
333
362
|
ws.removeAllListeners('open');
|
|
334
363
|
ws.removeAllListeners('message');
|
|
@@ -354,16 +383,20 @@ export const makeSocket = (config) => {
|
|
|
354
383
|
ev.removeAllListeners('connection.update');
|
|
355
384
|
ev.destroy?.();
|
|
356
385
|
};
|
|
357
|
-
const waitForSocketOpen =
|
|
386
|
+
const waitForSocketOpen = () => {
|
|
358
387
|
if (ws.isOpen) {
|
|
359
|
-
|
|
388
|
+
const resolved = Promise.resolve();
|
|
389
|
+
resolved.cancel = () => { };
|
|
390
|
+
return resolved;
|
|
360
391
|
}
|
|
361
392
|
if (ws.isClosed || ws.isClosing) {
|
|
362
|
-
|
|
393
|
+
const rejected = Promise.reject(new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }));
|
|
394
|
+
rejected.cancel = () => { };
|
|
395
|
+
return rejected;
|
|
363
396
|
}
|
|
364
397
|
let onOpen;
|
|
365
398
|
let onClose;
|
|
366
|
-
|
|
399
|
+
const promise = new Promise((resolve, reject) => {
|
|
367
400
|
onOpen = () => resolve(undefined);
|
|
368
401
|
onClose = mapWebSocketError(reject);
|
|
369
402
|
ws.on('open', onOpen);
|
|
@@ -374,6 +407,16 @@ export const makeSocket = (config) => {
|
|
|
374
407
|
ws.off('close', onClose);
|
|
375
408
|
ws.off('error', onClose);
|
|
376
409
|
});
|
|
410
|
+
// AunuBaileys (1.0.77, HB-167/HB-179): a caller that stops
|
|
411
|
+
// waiting (its own timeout won the race) can detach these
|
|
412
|
+
// listeners immediately instead of leaving them on the ws
|
|
413
|
+
// emitter until the socket happens to open/close/error.
|
|
414
|
+
promise.cancel = () => {
|
|
415
|
+
ws.off('open', onOpen);
|
|
416
|
+
ws.off('close', onClose);
|
|
417
|
+
ws.off('error', onClose);
|
|
418
|
+
};
|
|
419
|
+
return promise;
|
|
377
420
|
};
|
|
378
421
|
const keepAliveTick = () => {
|
|
379
422
|
if (!lastDateRecv) {
|
|
@@ -501,15 +544,55 @@ export const makeSocket = (config) => {
|
|
|
501
544
|
reject(new Boom(`requestPairingCode: socket did not open within ${PAIRING_READY_TIMEOUT_MS / 1000}s (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.timedOut }));
|
|
502
545
|
}, PAIRING_READY_TIMEOUT_MS);
|
|
503
546
|
});
|
|
547
|
+
const rawOpenWait = waitForSocketOpen();
|
|
504
548
|
try {
|
|
505
549
|
await Promise.race([
|
|
506
|
-
|
|
550
|
+
rawOpenWait.catch(() => {
|
|
507
551
|
throw new Boom(`requestPairingCode: connection closed while waiting for the socket to open (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed });
|
|
508
552
|
}),
|
|
509
553
|
closedWhileWaiting,
|
|
510
554
|
timedOut
|
|
511
555
|
]);
|
|
512
556
|
}
|
|
557
|
+
finally {
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
// AunuBaileys (1.0.77, HB-179): if the timeout or the close
|
|
560
|
+
// guard won the race, the socket-open waiter is still
|
|
561
|
+
// pending โ detach its ws listeners now instead of leaving
|
|
562
|
+
// them behind until the socket settles on its own.
|
|
563
|
+
rawOpenWait.cancel?.();
|
|
564
|
+
if (onUpdate) {
|
|
565
|
+
try {
|
|
566
|
+
ev.off('connection.update', onUpdate);
|
|
567
|
+
}
|
|
568
|
+
catch { }
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
// AunuBaileys (1.0.77, HB-213): the pairing IQ must not leave
|
|
573
|
+
// before the Noise handshake completes โ encodeFrame encrypts
|
|
574
|
+
// only once the handshake is finished, so an early IQ travels as
|
|
575
|
+
// a plaintext binary node; a strict server kills the connection
|
|
576
|
+
// over it while requestPairingCode still resolves with the code.
|
|
577
|
+
const waitForHandshakeComplete = async () => {
|
|
578
|
+
let onUpdate;
|
|
579
|
+
const closedWhileWaiting = new Promise((_, reject) => {
|
|
580
|
+
onUpdate = (update) => {
|
|
581
|
+
if (update.connection === 'close') {
|
|
582
|
+
reject(new Boom(`requestPairingCode: connection closed while waiting for the handshake to complete (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed }));
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
ev.on('connection.update', onUpdate);
|
|
586
|
+
});
|
|
587
|
+
let timer;
|
|
588
|
+
const timedOut = new Promise((_, reject) => {
|
|
589
|
+
timer = setTimeout(() => {
|
|
590
|
+
reject(new Boom(`requestPairingCode: handshake did not complete within ${PAIRING_READY_TIMEOUT_MS / 1000}s (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.timedOut }));
|
|
591
|
+
}, PAIRING_READY_TIMEOUT_MS);
|
|
592
|
+
});
|
|
593
|
+
try {
|
|
594
|
+
await Promise.race([handshakeComplete, closedWhileWaiting, timedOut]);
|
|
595
|
+
}
|
|
513
596
|
finally {
|
|
514
597
|
clearTimeout(timer);
|
|
515
598
|
if (onUpdate) {
|
|
@@ -534,13 +617,20 @@ export const makeSocket = (config) => {
|
|
|
534
617
|
pairingCodeRequestInFlight = true;
|
|
535
618
|
try {
|
|
536
619
|
await waitForPairingReady();
|
|
620
|
+
await waitForHandshakeComplete();
|
|
537
621
|
authState.creds.pairingCode = pairingCode;
|
|
538
622
|
authState.creds.me = {
|
|
539
623
|
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
|
540
624
|
name: '~'
|
|
541
625
|
};
|
|
542
626
|
ev.emit('creds.update', authState.creds);
|
|
543
|
-
|
|
627
|
+
// AunuBaileys (1.0.77, HB-191): query(), not sendNode() โ the
|
|
628
|
+
// companion_hello result must be awaited and error-checked.
|
|
629
|
+
// sendNode resolved when the bytes hit the wire, so a server
|
|
630
|
+
// rejection left the optimistic creds.me/pairingCode persisted
|
|
631
|
+
// and sent the next boot into a 401 loop; a rejection now
|
|
632
|
+
// lands in the catch below, which rolls the creds back.
|
|
633
|
+
await query({
|
|
544
634
|
tag: 'iq',
|
|
545
635
|
attrs: {
|
|
546
636
|
to: S_WHATSAPP_NET,
|
|
@@ -644,6 +734,16 @@ export const makeSocket = (config) => {
|
|
|
644
734
|
}
|
|
645
735
|
});
|
|
646
736
|
ws.on('error', mapWebSocketError(end));
|
|
737
|
+
// AunuBaileys (1.0.77, HB-189): the ws client forwards
|
|
738
|
+
// 'unexpected-response', which suppresses ws's own handshake
|
|
739
|
+
// abort โ without a listener here a non-101 response (403/502,
|
|
740
|
+
// ISP intercept page) wedges the socket in 'connecting' forever:
|
|
741
|
+
// no open, no error, no close, and the host never reconnects.
|
|
742
|
+
ws.on('unexpected-response', (_req, res) => {
|
|
743
|
+
const code = res?.statusCode;
|
|
744
|
+
const statusCode = Number.isFinite(code) && code >= 400 && code <= 599 ? code : 500;
|
|
745
|
+
end(new Boom(`Unexpected server response: ${code}`, { statusCode }));
|
|
746
|
+
});
|
|
647
747
|
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
648
748
|
// the server terminated the connection
|
|
649
749
|
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
@@ -662,6 +762,16 @@ export const makeSocket = (config) => {
|
|
|
662
762
|
}
|
|
663
763
|
};
|
|
664
764
|
await sendNode(iq);
|
|
765
|
+
// AunuBaileys (1.0.77, HB-243): a duplicate pair-device IQ
|
|
766
|
+
// (server retry) must not start a parallel QR chain โ both
|
|
767
|
+
// chains share the single qrTimer slot, so the orphaned first
|
|
768
|
+
// chain keeps cycling after login and its ref exhaustion ends
|
|
769
|
+
// the healthy, freshly logged-in connection.
|
|
770
|
+
if (qrChainActive) {
|
|
771
|
+
logger.warn('ignoring duplicate pair-device IQ: a QR chain is already running');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
qrChainActive = true;
|
|
665
775
|
const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
|
|
666
776
|
const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
|
|
667
777
|
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
|
|
@@ -670,6 +780,7 @@ export const makeSocket = (config) => {
|
|
|
670
780
|
let qrMs = qrTimeout || 60000; // time to let a QR live
|
|
671
781
|
const genPairQR = () => {
|
|
672
782
|
if (!ws.isOpen) {
|
|
783
|
+
qrChainActive = false;
|
|
673
784
|
return;
|
|
674
785
|
}
|
|
675
786
|
const refNode = refNodes.shift();
|
|
@@ -726,6 +837,7 @@ export const makeSocket = (config) => {
|
|
|
726
837
|
await sendPassiveIq('active');
|
|
727
838
|
logger.info('opened connection to WA');
|
|
728
839
|
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
840
|
+
qrChainActive = false;
|
|
729
841
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
730
842
|
ev.emit('connection.update', { connection: 'open' });
|
|
731
843
|
}
|
|
@@ -737,9 +849,9 @@ export const makeSocket = (config) => {
|
|
|
737
849
|
// AunuBaileys (1.0.71): 405 client_too_old โ fetch the live WA Web
|
|
738
850
|
// build so the caller's reconnect uses it (donor: violetix
|
|
739
851
|
// lib/Socket/socket.js CB:failure handler). Only for static versions;
|
|
740
|
-
// a resolver function re-resolves on the next connect anyway.
|
|
741
|
-
//
|
|
742
|
-
//
|
|
852
|
+
// a resolver function re-resolves on the next connect anyway. Both
|
|
853
|
+
// 405 paths (CB:failure and CB:stream:error) await this before
|
|
854
|
+
// end(), so the clientTooOld signal reaches the host first.
|
|
743
855
|
const handleClientTooOld = async () => {
|
|
744
856
|
if (typeof config.version === 'function') {
|
|
745
857
|
return;
|
|
@@ -759,11 +871,16 @@ export const makeSocket = (config) => {
|
|
|
759
871
|
logger.warn({ err }, 'failed to fetch the latest WA Web version after a 405');
|
|
760
872
|
}
|
|
761
873
|
};
|
|
762
|
-
ws.on('CB:stream:error', (node) => {
|
|
874
|
+
ws.on('CB:stream:error', async (node) => {
|
|
763
875
|
logger.error({ node }, 'stream errored out');
|
|
764
876
|
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
765
877
|
if (statusCode === 405) {
|
|
766
|
-
|
|
878
|
+
// AunuBaileys (1.0.77, HB-148): awaited like the CB:failure
|
|
879
|
+
// path (1.0.72) โ firing it void let end() tear the
|
|
880
|
+
// connection.update listeners down before the version
|
|
881
|
+
// fetch landed, so the clientTooOld signal never reached
|
|
882
|
+
// the host and a 405 looked like any other disconnect.
|
|
883
|
+
await handleClientTooOld();
|
|
767
884
|
}
|
|
768
885
|
end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }));
|
|
769
886
|
});
|
|
@@ -802,7 +919,7 @@ export const makeSocket = (config) => {
|
|
|
802
919
|
if (offlineDrainBatches >= OFFLINE_DRAIN_MAX_BATCHES) {
|
|
803
920
|
stopOfflineDrain();
|
|
804
921
|
logger.warn({ batches: offlineDrainBatches }, 'offline drain hit the batch cap; remaining backlog waits for the next reconnect');
|
|
805
|
-
return;
|
|
922
|
+
return false;
|
|
806
923
|
}
|
|
807
924
|
offlineDrainBatches += 1;
|
|
808
925
|
// AunuBaileys (1.0.70): a floating sendNode here could reject
|
|
@@ -812,12 +929,19 @@ export const makeSocket = (config) => {
|
|
|
812
929
|
attrs: {},
|
|
813
930
|
content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
|
|
814
931
|
}).catch(err => logger.warn({ err }, 'failed to request offline batch'));
|
|
932
|
+
return true;
|
|
815
933
|
};
|
|
816
934
|
const scheduleOfflineDrain = () => {
|
|
817
935
|
stopOfflineDrain();
|
|
818
936
|
offlineDrainTimer = setTimeout(() => {
|
|
819
|
-
|
|
820
|
-
|
|
937
|
+
// AunuBaileys (1.0.77, HB-109): stop re-arming at the batch
|
|
938
|
+
// cap โ the unconditional re-schedule used to undo
|
|
939
|
+
// requestOfflineBatch's stopOfflineDrain(), so when the
|
|
940
|
+
// terminal offline bulletin never arrived the cap warning
|
|
941
|
+
// repeated every 500ms for the life of the socket.
|
|
942
|
+
if (requestOfflineBatch()) {
|
|
943
|
+
scheduleOfflineDrain();
|
|
944
|
+
}
|
|
821
945
|
}, OFFLINE_DRAIN_INTERVAL_MS);
|
|
822
946
|
};
|
|
823
947
|
ws.on('CB:ib,,offline_preview', (node) => {
|
package/lib/Utils/generics.js
CHANGED
|
@@ -374,6 +374,16 @@ export const getCallStatusFromNode = ({ tag, attrs }) => {
|
|
|
374
374
|
};
|
|
375
375
|
const UNEXPECTED_SERVER_CODE_TEXT = 'Unexpected server response: ';
|
|
376
376
|
export const getCodeFromWSError = (error) => {
|
|
377
|
+
// AunuBaileys (1.0.77, HB-133): an error that already carries a
|
|
378
|
+
// valid Boom status code keeps it. Re-deriving the code from the
|
|
379
|
+
// message text lost real codes โ promiseTimeout's Boom
|
|
380
|
+
// 'Timed Out' (408) was re-wrapped as a 500 by mapWebSocketError
|
|
381
|
+
// because the textual timeout check below is case-sensitive
|
|
382
|
+
// ('Timed Out' vs 'timed out').
|
|
383
|
+
const existingCode = error?.output?.statusCode;
|
|
384
|
+
if (Number.isFinite(existingCode) && existingCode >= 400 && existingCode <= 599) {
|
|
385
|
+
return existingCode;
|
|
386
|
+
}
|
|
377
387
|
let statusCode = 500;
|
|
378
388
|
if (error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
|
|
379
389
|
const code = +error?.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length);
|
|
@@ -385,7 +395,7 @@ export const getCodeFromWSError = (error) => {
|
|
|
385
395
|
// AunuBaileys (1.0.67): error.code can be numeric (e.g. WS close
|
|
386
396
|
// 1006) โ only call startsWith on strings.
|
|
387
397
|
(typeof error?.code === 'string' && error.code.startsWith('E')) ||
|
|
388
|
-
error?.message?.includes('timed out')) {
|
|
398
|
+
error?.message?.toLowerCase().includes('timed out')) {
|
|
389
399
|
// handle ETIMEOUT, ENOTFOUND etc
|
|
390
400
|
statusCode = 408;
|
|
391
401
|
}
|
package/lib/Utils/make-mutex.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export const makeMutex = () => {
|
|
2
2
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
3
|
let task = Promise.resolve();
|
|
4
|
-
|
|
4
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ the old `taskTimeout` was dead
|
|
5
|
+
// code (declared, cleared in a finally, NEVER assigned โ there is no
|
|
6
|
+
// hidden timeout safety net, and inventing one that breaks the chain
|
|
7
|
+
// would corrupt the serialization this mutex exists for). Removed.
|
|
8
|
+
// Note: a task that never settles blocks later tasks by design โ that
|
|
9
|
+
// is what a serial mutex means (upstream parity); callers must not
|
|
10
|
+
// await unbounded work inside mutex sections.
|
|
5
11
|
return {
|
|
6
12
|
mutex(code) {
|
|
7
13
|
task = (async () => {
|
|
@@ -11,14 +17,8 @@ export const makeMutex = () => {
|
|
|
11
17
|
await task;
|
|
12
18
|
}
|
|
13
19
|
catch { }
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const result = await code();
|
|
17
|
-
return result;
|
|
18
|
-
}
|
|
19
|
-
finally {
|
|
20
|
-
clearTimeout(taskTimeout);
|
|
21
|
-
}
|
|
20
|
+
// execute the current task
|
|
21
|
+
return await code();
|
|
22
22
|
})();
|
|
23
23
|
// we replace the existing task, appending the new piece of execution to it
|
|
24
24
|
// so the next task will have to wait for this one to finish
|
|
@@ -601,11 +601,24 @@ const toSmallestChunkSize = (num) => {
|
|
|
601
601
|
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
|
|
602
602
|
};
|
|
603
603
|
export const getUrlFromDirectPath = (directPath, fallbackHost) => `https://${fallbackHost || DEF_HOST}${directPath}`;
|
|
604
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ WhatsApp media is only ever served
|
|
605
|
+
// from WhatsApp/Facebook CDN hosts. A sender-supplied URL whose host is
|
|
606
|
+
// anything else is not a media URL, it is an SSRF attempt (the bot would
|
|
607
|
+
// happily GET an internal https host with the sender's directPath glued
|
|
608
|
+
// on). Non-WA hosts are never fetched: with a directPath we fall back to
|
|
609
|
+
// the canonical media host (the path is globally valid there); without
|
|
610
|
+
// one the download is refused.
|
|
611
|
+
const WA_MEDIA_HOST_SUFFIXES = ['.whatsapp.net', '.fbcdn.net'];
|
|
612
|
+
const isWAMediaHost = (hostname) => !!hostname &&
|
|
613
|
+
(hostname === 'whatsapp.net' || hostname === 'fbcdn.net' ||
|
|
614
|
+
WA_MEDIA_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)));
|
|
604
615
|
export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
605
616
|
// AunuBaileys (1.0.69): the sender-supplied URL used to be trusted
|
|
606
617
|
// verbatim (host + path) โ a remote peer could trigger a GET to an
|
|
607
618
|
// arbitrary https host. Like upstream v7, only the host is taken
|
|
608
619
|
// from the URL; the path is rebuilt from our own directPath.
|
|
620
|
+
// AunuBaileys (1.0.76): the host is now ALSO allowlisted (above) โ
|
|
621
|
+
// a non-WA host falls back to mmg.whatsapp.net instead of being fetched.
|
|
609
622
|
let downloadUrl;
|
|
610
623
|
if (directPath) {
|
|
611
624
|
let host;
|
|
@@ -613,10 +626,18 @@ export const downloadContentFromMessage = async ({ mediaKey, directPath, url },
|
|
|
613
626
|
host = new URL(url || '').hostname || undefined;
|
|
614
627
|
}
|
|
615
628
|
catch { }
|
|
629
|
+
if (!isWAMediaHost(host)) {
|
|
630
|
+
host = undefined;
|
|
631
|
+
}
|
|
616
632
|
downloadUrl = getUrlFromDirectPath(directPath, host);
|
|
617
633
|
}
|
|
618
634
|
else {
|
|
619
|
-
|
|
635
|
+
let parsed;
|
|
636
|
+
try {
|
|
637
|
+
parsed = new URL(url || '');
|
|
638
|
+
}
|
|
639
|
+
catch { }
|
|
640
|
+
downloadUrl = parsed?.protocol === 'https:' && isWAMediaHost(parsed.hostname) ? url : undefined;
|
|
620
641
|
}
|
|
621
642
|
if (!downloadUrl) {
|
|
622
643
|
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 });
|
|
@@ -628,7 +649,7 @@ export const downloadContentFromMessage = async ({ mediaKey, directPath, url },
|
|
|
628
649
|
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
|
|
629
650
|
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
|
630
651
|
* */
|
|
631
|
-
export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte, options } = {}) => {
|
|
652
|
+
export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv, macKey }, { startByte, endByte, options } = {}) => {
|
|
632
653
|
let bytesFetched = 0;
|
|
633
654
|
let startChunk = 0;
|
|
634
655
|
let firstBlockIsIV = false;
|
|
@@ -661,6 +682,35 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
661
682
|
});
|
|
662
683
|
let remainingBytes = Buffer.from([]);
|
|
663
684
|
let aes;
|
|
685
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ the downloaded ciphertext used
|
|
686
|
+
// to be decrypted with NO integrity check: the 10-byte HMAC-SHA256
|
|
687
|
+
// (macKey, iv || ciphertext) that WhatsApp appends to every media
|
|
688
|
+
// file was silently dropped with the trailing partial block, so a
|
|
689
|
+
// tampering server/CDN could alter media undetected (AES-CBC padding
|
|
690
|
+
// only catches damage to the final block). For FULL downloads (no
|
|
691
|
+
// byte range) with a macKey available, verify it: HMAC every
|
|
692
|
+
// ciphertext byte except a held-back final 10, compare in final().
|
|
693
|
+
// Ranged downloads fetch a slice that excludes the MAC โ they skip
|
|
694
|
+
// verification, exactly like before. Consumers of the stream still
|
|
695
|
+
// receive plaintext progressively, but a failed check errors the
|
|
696
|
+
// stream in final(), so buffered consumers (downloadMediaMessage
|
|
697
|
+
// 'buffer') discard the content wholesale.
|
|
698
|
+
const verifyMac = !!macKey && !startByte && !endByte;
|
|
699
|
+
const macHmac = verifyMac ? Crypto.createHmac('sha256', macKey).update(iv) : null;
|
|
700
|
+
let macTail = Buffer.alloc(0);
|
|
701
|
+
const feedMac = (chunk) => {
|
|
702
|
+
if (!macHmac) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const joined = macTail.length ? Buffer.concat([macTail, chunk]) : chunk;
|
|
706
|
+
if (joined.length > 10) {
|
|
707
|
+
macHmac.update(joined.subarray(0, joined.length - 10));
|
|
708
|
+
macTail = joined.subarray(joined.length - 10);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
macTail = joined;
|
|
712
|
+
}
|
|
713
|
+
};
|
|
664
714
|
const pushBytes = (bytes, push) => {
|
|
665
715
|
if (startByte || endByte) {
|
|
666
716
|
const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);
|
|
@@ -680,6 +730,7 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
680
730
|
};
|
|
681
731
|
const output = new Transform({
|
|
682
732
|
transform(chunk, _, callback) {
|
|
733
|
+
feedMac(chunk);
|
|
683
734
|
let data = Buffer.concat([remainingBytes, chunk]);
|
|
684
735
|
const decryptLength = toSmallestChunkSize(data.length);
|
|
685
736
|
remainingBytes = data.slice(decryptLength);
|
|
@@ -715,11 +766,19 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
715
766
|
}
|
|
716
767
|
try {
|
|
717
768
|
pushBytes(aes.final(), b => this.push(b));
|
|
718
|
-
callback();
|
|
719
769
|
}
|
|
720
770
|
catch (error) {
|
|
721
771
|
callback(error);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (macHmac) {
|
|
775
|
+
const expectedMac = macHmac.digest().subarray(0, 10);
|
|
776
|
+
if (macTail.length !== 10 || !Crypto.timingSafeEqual(Buffer.from(macTail), expectedMac)) {
|
|
777
|
+
callback(new Boom('Downloaded media failed HMAC integrity verification โ the file was tampered with or truncated in transit; refusing it', { statusCode: 400 }));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
722
780
|
}
|
|
781
|
+
callback();
|
|
723
782
|
}
|
|
724
783
|
});
|
|
725
784
|
// AunuBaileys (1.0.71): stream.pipe() does NOT forward source
|
|
@@ -15,8 +15,8 @@ const generateIV = (counter) => {
|
|
|
15
15
|
// corrupt header either wedges the stream forever (size 0: the header
|
|
16
16
|
// is never consumed, so every later call re-reads it) or buffers
|
|
17
17
|
// without bound waiting for megabytes that never arrive. Fail loudly
|
|
18
|
-
// instead: the
|
|
19
|
-
//
|
|
18
|
+
// instead: the throw is marked frame-fatal (1.0.77, HB-185) and the
|
|
19
|
+
// caller ends the socket, so the host reconnects at once.
|
|
20
20
|
const MAX_NOISE_FRAME_SIZE = 10 * 1024 * 1024;
|
|
21
21
|
export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publicKey }, NOISE_HEADER, logger, routingInfo }) => {
|
|
22
22
|
logger = logger.child({ class: 'ns' });
|
|
@@ -161,23 +161,55 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
|
|
|
161
161
|
}
|
|
162
162
|
if (size > MAX_NOISE_FRAME_SIZE) {
|
|
163
163
|
// AunuBaileys (1.0.73, HB-91): never wait for a
|
|
164
|
-
// frame that cannot exist
|
|
165
|
-
//
|
|
166
|
-
// the
|
|
167
|
-
//
|
|
168
|
-
|
|
164
|
+
// frame that cannot exist. (1.0.77, HB-185): the
|
|
165
|
+
// throw alone did NOT end the socket โ the caller
|
|
166
|
+
// only logged it, the header was never consumed,
|
|
167
|
+
// and the stream wedged until the keep-alive
|
|
168
|
+
// watchdog fired ~65s later. Marked frame-fatal:
|
|
169
|
+
// the caller now ends the connection on it.
|
|
170
|
+
const err = new Boom(`noise frame too large: ${size} bytes`, { statusCode: 400 });
|
|
171
|
+
err.isFrameFatal = true;
|
|
172
|
+
throw err;
|
|
169
173
|
}
|
|
170
174
|
if (inBytes.length < size + 3) {
|
|
171
175
|
break;
|
|
172
176
|
}
|
|
173
177
|
let frame = inBytes.slice(3, size + 3);
|
|
174
178
|
inBytes = inBytes.slice(size + 3);
|
|
179
|
+
let decoded = true;
|
|
175
180
|
if (isFinished) {
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
let result;
|
|
182
|
+
try {
|
|
183
|
+
result = decrypt(frame);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
// AunuBaileys (1.0.77, HB-207): a GCM failure
|
|
187
|
+
// leaves readCounter behind the server's
|
|
188
|
+
// counter, so every later frame decrypts with
|
|
189
|
+
// the wrong IV and fails too โ the stream can
|
|
190
|
+
// never resync. Frame-fatal: the caller ends
|
|
191
|
+
// the connection instead of losing every
|
|
192
|
+
// inbound message until the watchdog fires.
|
|
193
|
+
err.isFrameFatal = true;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
frame = await decodeBinaryNode(result);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// AunuBaileys (1.0.77): stanza-level corruption
|
|
201
|
+
// is NOT fatal โ the frame was consumed and the
|
|
202
|
+
// counter advanced, so the stream is still in
|
|
203
|
+
// sync. Log and skip the frame instead of
|
|
204
|
+
// aborting the rest of the buffered frames.
|
|
205
|
+
logger.error({ err }, 'failed to decode a stanza; skipping the frame');
|
|
206
|
+
decoded = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (decoded) {
|
|
210
|
+
logger.trace({ msg: frame?.attrs?.id }, 'recv frame');
|
|
211
|
+
onFrame(frame);
|
|
178
212
|
}
|
|
179
|
-
logger.trace({ msg: frame?.attrs?.id }, 'recv frame');
|
|
180
|
-
onFrame(frame);
|
|
181
213
|
size = getBytesSize();
|
|
182
214
|
}
|
|
183
215
|
}
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// - 'connection.update' { sessionWatchdog: { quietForMs, lastInboundAt, at } }
|
|
9
9
|
// once per quiet episode (re-armed by the next inbound message).
|
|
10
10
|
// Inbound traffic is observed on the event buffer ('messages.upsert' /
|
|
11
|
-
// 'messages.update')
|
|
12
|
-
//
|
|
11
|
+
// 'messages.update') AND โ since 1.0.76 โ on the raw WebSocket frame
|
|
12
|
+
// stream when sock.ws is reachable (buffered delivery is deferred during
|
|
13
|
+
// long syncs, which used to read as a false deaf session); hosts can
|
|
14
|
+
// also call watchdog.markInbound() directly. Wiring is opt-in: lib/Socket/index.js
|
|
13
15
|
// attaches it only when config.sessionWatchdog is set (object = options,
|
|
14
16
|
// true = defaults). The interval timer is unref'd so it never keeps a
|
|
15
17
|
// process alive on its own.
|
|
@@ -84,6 +86,12 @@ export const makeSessionWatchdog = (sock, options = {}) => {
|
|
|
84
86
|
ev.off?.('messages.upsert', markInbound);
|
|
85
87
|
ev.off?.('messages.update', markInbound);
|
|
86
88
|
ev.off?.('connection.update', onConnectionUpdate);
|
|
89
|
+
if (rawWs) {
|
|
90
|
+
rawWs.off?.('message', markInbound);
|
|
91
|
+
if (!rawWs.off && typeof rawWs.removeListener === 'function') {
|
|
92
|
+
rawWs.removeListener('message', markInbound);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
87
95
|
return watchdog;
|
|
88
96
|
};
|
|
89
97
|
const getState = () => ({
|
|
@@ -95,6 +103,19 @@ export const makeSessionWatchdog = (sock, options = {}) => {
|
|
|
95
103
|
ev.on('messages.upsert', markInbound);
|
|
96
104
|
ev.on('messages.update', markInbound);
|
|
97
105
|
ev.on('connection.update', onConnectionUpdate);
|
|
106
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ ALSO observe the raw WebSocket
|
|
107
|
+
// frame stream when available. messages.upsert/update travel through
|
|
108
|
+
// the event buffer, which DEFERS delivery to listeners while a
|
|
109
|
+
// buffered function runs (app-state resync, history processing): real
|
|
110
|
+
// inbound messages then fail to re-arm the watchdog for the whole
|
|
111
|
+
// buffering window and a long sync reads as a deaf session โ a false
|
|
112
|
+
// alert. Raw frames are never buffered, and for a deaf-session
|
|
113
|
+
// detector any inbound frame (message, receipt, IQ, keep-alive reply)
|
|
114
|
+
// is the truer liveness signal anyway. Detached again in stop().
|
|
115
|
+
const rawWs = sock.ws && typeof sock.ws.on === 'function' ? sock.ws : null;
|
|
116
|
+
if (rawWs) {
|
|
117
|
+
rawWs.on('message', markInbound);
|
|
118
|
+
}
|
|
98
119
|
const watchdog = {
|
|
99
120
|
markInbound,
|
|
100
121
|
noteInbound: markInbound,
|
|
@@ -45,7 +45,14 @@ export const useMultiFileAuthState = async (folder, logger) => {
|
|
|
45
45
|
try {
|
|
46
46
|
// AunuBaileys: atomic write (tmp + rename) anti-korupsi sesi โ terinspirasi @itsliaaa/baileys
|
|
47
47
|
const tmpPath = `${filePath}.tmp-${process.pid}`;
|
|
48
|
-
|
|
48
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ these files hold
|
|
49
|
+
// private keys (creds.json, session/sender-key records).
|
|
50
|
+
// They used to be created with default permissions (0644
|
|
51
|
+
// on a typical umask) โ readable by every local user on a
|
|
52
|
+
// shared host. Write the tmp file 0600; the rename carries
|
|
53
|
+
// the mode onto the target, so pre-existing 0644 files
|
|
54
|
+
// also self-heal to 0600 on their next save.
|
|
55
|
+
await writeFile(tmpPath, JSON.stringify(data, BufferJSON.replacer), { mode: 0o600 });
|
|
49
56
|
await rename(tmpPath, filePath);
|
|
50
57
|
}
|
|
51
58
|
catch (err) {
|
package/lib/WABinary/decode.js
CHANGED
|
@@ -3,9 +3,24 @@ import { inflate } from 'zlib';
|
|
|
3
3
|
import * as constants from './constants.js';
|
|
4
4
|
import { jidEncode } from './jid-utils.js';
|
|
5
5
|
const inflatePromise = promisify(inflate);
|
|
6
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ inflate used to run with NO output
|
|
7
|
+
// cap: zlib expands up to ~1000x, so a max-size wire frame (10 MiB, HB-91)
|
|
8
|
+
// could inflate to ~10 GB and OOM the process. 128 MiB is far above any
|
|
9
|
+
// legitimate stanza (wire stanzas are messages / IQ results / app-state
|
|
10
|
+
// patches โ big payloads travel as media downloads, not stanzas) while
|
|
11
|
+
// still stopping a bomb cold. Exceeding it is a clean decode failure.
|
|
12
|
+
const MAX_DECOMPRESSED_SIZE = 128 * 1024 * 1024;
|
|
6
13
|
export const decompressingIfRequired = async (buffer) => {
|
|
7
14
|
if (2 & buffer.readUInt8()) {
|
|
8
|
-
|
|
15
|
+
try {
|
|
16
|
+
buffer = await inflatePromise(buffer.slice(1), { maxOutputLength: MAX_DECOMPRESSED_SIZE });
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err?.code === 'ERR_BUFFER_TOO_LARGE') {
|
|
20
|
+
throw new Error(`AunuBaileys: compressed stanza exceeds the ${MAX_DECOMPRESSED_SIZE / (1024 * 1024)} MiB decompressed-size cap โ refusing to inflate (possible zlib bomb)`);
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
9
24
|
}
|
|
10
25
|
else {
|
|
11
26
|
// nodes with no compression have a 0x00 prefix, we remove that
|
|
@@ -13,7 +28,18 @@ export const decompressingIfRequired = async (buffer) => {
|
|
|
13
28
|
}
|
|
14
29
|
return buffer;
|
|
15
30
|
};
|
|
16
|
-
|
|
31
|
+
// AunuBaileys (1.0.76): Hunt-ronde-3 โ the decoder recursed with NO depth
|
|
32
|
+
// bound: ~5 wire bytes buy one nesting level, so a ~100 KB stanza reached
|
|
33
|
+
// depth 20,000 and died as an uncontrolled RangeError (stack overflow).
|
|
34
|
+
// Legit stanzas nest < ~20 deep; 512 is generous headroom, and crossing it
|
|
35
|
+
// is a descriptive decode error instead of a V8 crash. (Total node count
|
|
36
|
+
// needs no separate cap: every node consumes >= 1 input byte, so count is
|
|
37
|
+
// bounded by the โ now also capped โ buffer length.)
|
|
38
|
+
const MAX_NODE_DEPTH = 512;
|
|
39
|
+
export const decodeDecompressedBinaryNode = (buffer, opts, indexRef = { index: 0 }, depth = 0) => {
|
|
40
|
+
if (depth > MAX_NODE_DEPTH) {
|
|
41
|
+
throw new Error(`AunuBaileys: binary node nesting exceeds the ${MAX_NODE_DEPTH}-level safety cap โ refusing to decode (possible depth bomb)`);
|
|
42
|
+
}
|
|
17
43
|
const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts;
|
|
18
44
|
const checkEOS = (length) => {
|
|
19
45
|
if (indexRef.index + length > buffer.length) {
|
|
@@ -186,7 +212,7 @@ export const decodeDecompressedBinaryNode = (buffer, opts, indexRef = { index: 0
|
|
|
186
212
|
const items = [];
|
|
187
213
|
const size = readListSize(tag);
|
|
188
214
|
for (let i = 0; i < size; i++) {
|
|
189
|
-
items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef));
|
|
215
|
+
items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef, depth + 1));
|
|
190
216
|
}
|
|
191
217
|
return items;
|
|
192
218
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aunucore/baileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.77",
|
|
5
5
|
"description": "AunuBaileys โ the self-healing WhatsApp Web API for Node.js. Bad MAC and \"2000 messages into the future\" errors repair themselves, LID and phone-number identities stay in sync, anti-ban is built in, pairing codes are custom โ plus 29 experimental features no other Baileys fork ships: invisible watermarks, per-person tag-all cards, side-profile text, ghost mentions and more. ESM, CJS and full TypeScript types.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|