whalibmob 5.14.11 → 5.14.12

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.
Files changed (2) hide show
  1. package/lib/Client.js +124 -38
  2. package/package.json +1 -1
package/lib/Client.js CHANGED
@@ -6,6 +6,7 @@ const EventEmitter = require('events');
6
6
  const path = require('path');
7
7
  const fs = require('fs');
8
8
  const crypto = require('crypto');
9
+ const { Mutex } = require('async-mutex');
9
10
  const { NoiseSocket } = require('./noise');
10
11
  const { MessageSender, generateMessageId, makeJid, buildOrGetAdvIdentity } = require('./messages/MessageSender');
11
12
  const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, assertRegistrationKeys, fetchIosVersion, fetchWaVersion } = require('./Registration');
@@ -368,6 +369,33 @@ class WhalibmobClient extends EventEmitter {
368
369
  this._numberFixAttempted = false;
369
370
  this._pingTimer = null;
370
371
  this._keepTimer = null;
372
+ // Both pre-key checks ask the server a question, wait for the answer, and
373
+ // top the account up if they do not like it. Three things start them —
374
+ // login, the half-hourly timer, and the server saying it is running low —
375
+ // and nothing stopped two from being in the air at once. Overlapping runs
376
+ // each saw the same shortage and each answered it, so the account
377
+ // generated and uploaded two batches for one gap. Serialised, the second
378
+ // run asks after the first has finished: the count it gets back is the one
379
+ // the first run just produced, so it stands down instead of repeating it.
380
+ // One mutex per client — two accounts in the same process are unrelated
381
+ // and must not wait on each other.
382
+ this._preKeyMutex = new Mutex();
383
+ // Incoming work runs one item at a time, in arrival order.
384
+ //
385
+ // Stanzas arrive faster than they are handled, and the handling is
386
+ // asynchronous, so without these each one raced the ones before it: two
387
+ // messages from the same contact could decrypt at once and advance the
388
+ // same ratchet from the same starting point, and two retry receipts could
389
+ // tear a session down while the resend for the previous one was still
390
+ // building it back up.
391
+ //
392
+ // Three separate locks, not one. A message whose decryption waits on
393
+ // something that only arrives as a notification would deadlock against a
394
+ // shared lock — the notification could never be processed to release it.
395
+ // Keeping them apart means each queue only ever waits on its own kind.
396
+ this._messageMutex = new Mutex();
397
+ this._receiptMutex = new Mutex();
398
+ this._notificationMutex = new Mutex();
371
399
  this._connected = false;
372
400
  this._reconnecting = false;
373
401
  this._reconnectTry = 0;
@@ -2131,6 +2159,16 @@ class WhalibmobClient extends EventEmitter {
2131
2159
 
2132
2160
  // ─── Message handling ─────────────────────────────────────────────────────
2133
2161
 
2162
+ // Put work on one of the incoming queues.
2163
+ //
2164
+ // Never rejects, and never awaited by the caller: the socket reader must keep
2165
+ // reading, and one stanza that fails must not take the ones queued behind it
2166
+ // with it. What the lock buys is order, not delivery of a result.
2167
+ _queueIncoming(mutex, label, fn) {
2168
+ return mutex.runExclusive(fn).catch(err =>
2169
+ _whaDbg('[DBG] ' + label + '_QUEUE_ERR ' + (err && err.message)));
2170
+ }
2171
+
2134
2172
  async _handleMessage(node) {
2135
2173
  const attrs = node.attrs || {};
2136
2174
  const fromRaw = attrs.from || null;
@@ -2203,22 +2241,29 @@ class WhalibmobClient extends EventEmitter {
2203
2241
  }
2204
2242
  }
2205
2243
 
2244
+ // The acknowledgement goes out first and stays outside the queue below.
2245
+ // It tells the server the stanza arrived, and it has to be prompt whatever
2246
+ // the decryption goes on to do — holding it behind a queue of other
2247
+ // people's messages is how a client stops looking like it is listening.
2206
2248
  if (skmsgNode && isGroup) {
2207
2249
  this._sendMessageAck(id, fromRaw, partRaw);
2208
2250
  this._sendDeliveryReceipt(id, fromRaw, partRaw);
2209
- // SKDM must be processed (awaited) before we attempt skmsg decryption,
2210
- // because the first message from a sender always bundles both together.
2211
- if (participantsNode) {
2212
- await this._processSKDMDistribution(from, participant, participantsNode);
2213
- }
2214
- this._decryptGroupMessage({ node, from, participant, fromRaw, partRaw, id, ts, skmsgNode });
2251
+ this._queueIncoming(this._messageMutex, 'MSG', async () => {
2252
+ // SKDM must be processed (awaited) before we attempt skmsg decryption,
2253
+ // because the first message from a sender always bundles both together.
2254
+ if (participantsNode) {
2255
+ await this._processSKDMDistribution(from, participant, participantsNode);
2256
+ }
2257
+ await this._decryptGroupMessage({ node, from, participant, fromRaw, partRaw, id, ts, skmsgNode });
2258
+ });
2215
2259
  return;
2216
2260
  }
2217
2261
 
2218
2262
  if (dmEncNode && this._signal) {
2219
2263
  this._sendMessageAck(id, fromRaw, partRaw);
2220
2264
  this._sendDeliveryReceipt(id, fromRaw, partRaw);
2221
- this._decryptDMMessage({ node, from, participant, fromRaw, partRaw, senderPn, id, ts, dmEncNode });
2265
+ this._queueIncoming(this._messageMutex, 'MSG', () =>
2266
+ this._decryptDMMessage({ node, from, participant, fromRaw, partRaw, senderPn, id, ts, dmEncNode }));
2222
2267
  return;
2223
2268
  }
2224
2269
 
@@ -2335,7 +2380,10 @@ class WhalibmobClient extends EventEmitter {
2335
2380
  (candidates.length > 1 ? ' alts=' + candidates.slice(1).join(',') : '') +
2336
2381
  ' cipherLen=' + cipherBuf.length);
2337
2382
 
2338
- this._decryptWithCandidates(candidates, encType, cipherBuf, id)
2383
+ // Returned, not discarded: _handleMessage runs this behind the message
2384
+ // queue, and the queue can only hold the next message back for as long as
2385
+ // it can see this one is still running.
2386
+ return this._decryptWithCandidates(candidates, encType, cipherBuf, id)
2339
2387
  .then(async plaintext => {
2340
2388
  this._resolveRetry(id);
2341
2389
  const decoded = this._decodeMsg(plaintext);
@@ -2406,7 +2454,8 @@ class WhalibmobClient extends EventEmitter {
2406
2454
  ? skmsgNode.content
2407
2455
  : Buffer.from(skmsgNode.content || '');
2408
2456
 
2409
- this._signal.senderKeyDecrypt(from, participant, cipherBuf)
2457
+ // Returned for the same reason as the direct-message path above.
2458
+ return this._signal.senderKeyDecrypt(from, participant, cipherBuf)
2410
2459
  .then(plaintext => {
2411
2460
  this._resolveRetry(id);
2412
2461
  const decoded = this._decodeMsg(plaintext);
@@ -2555,36 +2604,51 @@ class WhalibmobClient extends EventEmitter {
2555
2604
 
2556
2605
  _whaDbg('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr +
2557
2606
  ' — clearing session + resending\n');
2558
- this._purgeContactSessions(recipientPhone, 'RETRY');
2559
-
2560
- // Re-send the message — will create fresh pre-key (pkmsg) sessions.
2561
- // A group resend must go back through the group path: it needs a fresh
2562
- // SenderKey distribution, which only _sendGroupMessage builds. Drop the
2563
- // "already distributed" record first, or the resend would carry the same
2564
- // bare skmsg the recipient just told us it could not read.
2565
- // Carry the round count into the resend so its own retry receipts continue
2566
- // the same chain instead of restarting it at zero.
2567
- const opts = Object.assign({}, cached.options, { _retryDepth: depth });
2568
-
2569
- if (cached.isGroup) {
2570
- const resendId = generateMessageId();
2571
- if (this._signal && this._signal.senderKeyStore) {
2572
- this._signal.senderKeyStore.invalidateSKDM(cached.toJid);
2607
+
2608
+ // Everything above this point stays synchronous on purpose. Every device
2609
+ // that could not read the message sends its own receipt, and the cache
2610
+ // entry taken above is what makes only the first of them resend — put an
2611
+ // await in front of it and they all pass the check together.
2612
+ //
2613
+ // From here on it is one at a time. Tearing a contact's sessions down and
2614
+ // building them back up is not something two receipts can be doing at once:
2615
+ // the second purge lands in the middle of the first resend and deletes the
2616
+ // session it had just created, so the resend goes out under a session the
2617
+ // recipient no longer has and draws another retry.
2618
+ return this._queueIncoming(this._receiptMutex, 'RETRY', async () => {
2619
+ this._purgeContactSessions(recipientPhone, 'RETRY');
2620
+
2621
+ // Re-send the message — will create fresh pre-key (pkmsg) sessions.
2622
+ // A group resend must go back through the group path: it needs a fresh
2623
+ // SenderKey distribution, which only _sendGroupMessage builds. Drop the
2624
+ // "already distributed" record first, or the resend would carry the same
2625
+ // bare skmsg the recipient just told us it could not read.
2626
+ // Carry the round count into the resend so its own retry receipts continue
2627
+ // the same chain instead of restarting it at zero.
2628
+ const opts = Object.assign({}, cached.options, { _retryDepth: depth });
2629
+
2630
+ if (cached.isGroup) {
2631
+ const resendId = generateMessageId();
2632
+ if (this._signal && this._signal.senderKeyStore) {
2633
+ this._signal.senderKeyStore.invalidateSKDM(cached.toJid);
2634
+ }
2635
+ _whaDbg('[DBG] RETRY_RESEND group round=' + depth + ' newId=' + resendId + '\n');
2636
+ // Awaited inside the queue, so the next receipt's purge cannot land
2637
+ // while this resend is still choosing and building sessions.
2638
+ await this._sender._sendGroupMessage(
2639
+ cached.toJid, resendId, cached.plaintext, cached.mediaType, opts
2640
+ )
2641
+ .then(r => _whaDbg('[DBG] RETRY_RESEND group ok id=' + (r && r.id) + '\n'))
2642
+ .catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
2643
+ return;
2573
2644
  }
2574
- _whaDbg('[DBG] RETRY_RESEND group round=' + depth + ' newId=' + resendId + '\n');
2575
- this._sender._sendGroupMessage(
2576
- cached.toJid, resendId, cached.plaintext, cached.mediaType, opts
2645
+
2646
+ await this._sender._sendDMMessage(
2647
+ cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, opts
2577
2648
  )
2578
- .then(r => _whaDbg('[DBG] RETRY_RESEND group ok id=' + (r && r.id) + '\n'))
2649
+ .then(r => _whaDbg('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
2579
2650
  .catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
2580
- return;
2581
- }
2582
-
2583
- this._sender._sendDMMessage(
2584
- cached.toJid, cached.msgId, cached.plaintext, cached.mediaType, opts
2585
- )
2586
- .then(r => _whaDbg('[DBG] RETRY_RESEND ok id=' + r.id + '\n'))
2587
- .catch(e => _whaDbg('[DBG] RETRY_RESEND_ERR: ' + e.message));
2651
+ });
2588
2652
  }
2589
2653
 
2590
2654
  _handleAck(node) {
@@ -3090,9 +3154,13 @@ class WhalibmobClient extends EventEmitter {
3090
3154
  // cleared, so a LID with no known number simply gets looked up the next
3091
3155
  // time something sends to it.
3092
3156
  if (this._signal && fromPhone) {
3093
- setImmediate(() => {
3157
+ // Queued rather than fired loose. A contact that changes devices
3158
+ // several times in a row — leaving and rejoining a group does it —
3159
+ // sends one of these per change, and two overlapping warm-ups race
3160
+ // each other's session building for the same contact.
3161
+ this._queueIncoming(this._notificationMutex, 'NOTIF', async () => {
3094
3162
  if (!this._connected || !this._devMgr) return;
3095
- this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
3163
+ await this._devMgr.bulkEnsureSessions([fromPhone], this._signal, false)
3096
3164
  .then(() => _whaDbg('[DBG] NOTIF_DEVICES bg-usync done for ' + fromPhone))
3097
3165
  .catch(e => _whaDbg('[DBG] NOTIF_DEVICES bg-usync err: ' + e.message));
3098
3166
  });
@@ -3490,7 +3558,18 @@ class WhalibmobClient extends EventEmitter {
3490
3558
  // the signed pre-key it is serving — so nothing here depends on guessing how
3491
3559
  // it hashes them. A mismatch means the bundle other people are handed is not
3492
3560
  // the one this session can answer for, and no amount of waiting fixes that.
3561
+ //
3562
+ // Shares _preKeyMutex with _ensureServerPreKeys: both end in the same
3563
+ // replenish-and-upload, so protecting only one of them would leave the same
3564
+ // double upload reachable through the other.
3493
3565
  async _verifyServerKeyBundle(reason) {
3566
+ if (!this._signal) return;
3567
+ return this._preKeyMutex.runExclusive(() => this._verifyServerKeyBundleLocked(reason));
3568
+ }
3569
+
3570
+ async _verifyServerKeyBundleLocked(reason) {
3571
+ // Re-checked inside the lock: the session can be released while a caller
3572
+ // is still waiting its turn.
3494
3573
  if (!this._signal) return;
3495
3574
  try {
3496
3575
  const digest = await this._queryKeyDigest();
@@ -3542,7 +3621,14 @@ class WhalibmobClient extends EventEmitter {
3542
3621
  // Top the server back up when it is running out. Never throws: a session that
3543
3622
  // cannot ask is left exactly as it was rather than losing its connection over
3544
3623
  // a bookkeeping query.
3624
+ //
3625
+ // Runs one at a time — see _preKeyMutex in the constructor.
3545
3626
  async _ensureServerPreKeys(reason) {
3627
+ if (!this._signal) return;
3628
+ return this._preKeyMutex.runExclusive(() => this._ensureServerPreKeysLocked(reason));
3629
+ }
3630
+
3631
+ async _ensureServerPreKeysLocked(reason) {
3546
3632
  if (!this._signal) return;
3547
3633
  try {
3548
3634
  const serverCount = await this._queryServerPreKeyCount();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.14.11",
3
+ "version": "5.14.12",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",