whalibmob 5.12.16 → 5.12.17
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 +116 -16
- package/cli.js +11 -0
- package/lib/Client.js +20 -0
- package/lib/DeviceConfig.js +4 -3
- package/lib/Registration.js +48 -4
- package/lib/Store.js +24 -4
- package/lib/constants.js +63 -25
- package/lib/noise.js +102 -24
- package/lib/webproto.js +5 -2
- package/package.json +2 -1
- package/tools/diagnose-405.js +153 -0
package/README.md
CHANGED
|
@@ -255,6 +255,7 @@ npm install -g whalibmob
|
|
|
255
255
|
- [Custom Device Fields](#custom-device-fields)
|
|
256
256
|
- [Version & Token Overrides](#version--token-overrides)
|
|
257
257
|
- [When the server answers 405 on connect](#when-the-server-answers-405-on-connect)
|
|
258
|
+
- [Finding out what a 405 objects to](#finding-out-what-a-405-objects-to)
|
|
258
259
|
|
|
259
260
|
---
|
|
260
261
|
|
|
@@ -399,6 +400,14 @@ Use a custom session directory with `--session`:
|
|
|
399
400
|
wa connect 919634847671 --session /data/my-sessions
|
|
400
401
|
```
|
|
401
402
|
|
|
403
|
+
> [!IMPORTANT]
|
|
404
|
+
> **If this is refused with `405`, do not re-register the number.** The account
|
|
405
|
+
> is fine; the server declined the version the connect announced. Check for a
|
|
406
|
+
> `WA_VERSION` in your shell or in a `.env` file in the directory you ran the
|
|
407
|
+
> command from — it overrides the version the session registered with, and a
|
|
408
|
+
> stale one left there refuses every connect from that directory. See
|
|
409
|
+
> [When the server answers 405 on connect](#when-the-server-answers-405-on-connect).
|
|
410
|
+
|
|
402
411
|
### CLI Pairing Code
|
|
403
412
|
|
|
404
413
|
If the number is already in use on a phone, or the verification SMS never arrives, link to the existing account instead. When it is not obvious which way you mean, `wa connect` asks:
|
|
@@ -2163,7 +2172,7 @@ Every event from the SMS primary API fires here too — `message`, `receipt`, `p
|
|
|
2163
2172
|
| `pair_device` | `{ refs }` | the QR path produced reference strings |
|
|
2164
2173
|
| `history_sync` | `{ syncTypeName, chats, contacts, pushNames, merged }` | a chunk of history arrived |
|
|
2165
2174
|
| `history_sync_error` | `{ err, notification }` | a chunk could not be fetched or decrypted |
|
|
2166
|
-
| `client_rejected` | `{ reason, location, message }` | the server refused the client itself, not the session — `405` means the announced version is not accepted. Distinct from `auth_failure`, and there is nothing to re-pair. |
|
|
2175
|
+
| `client_rejected` | `{ reason, location, message }` | the server refused the client itself, not the session — `405` means the announced version is not accepted. Fires whether the refusal arrives during the handshake or once the stream is open, and the client stops retrying either way. Distinct from `auth_failure`, and there is nothing to re-pair. |
|
|
2167
2176
|
|
|
2168
2177
|
### Reading What the Phone Sent
|
|
2169
2178
|
|
|
@@ -4467,47 +4476,138 @@ These variables override individual fields on top of the selected profile:
|
|
|
4467
4476
|
|
|
4468
4477
|
| Variable | Description |
|
|
4469
4478
|
|---|---|
|
|
4470
|
-
| `WA_VERSION` | Pin the WhatsApp version (e.g. `2.24.13.80`). Skips the live store fetch, and is announced on connect in place of the version stored in the session. |
|
|
4479
|
+
| `WA_VERSION` | Pin the WhatsApp version (e.g. `2.24.13.80`). Skips the live store fetch, and is announced on connect **in place of the version stored in the session**. The CLI also reads it from a `.env` file in the working directory, so one left there is announced by every connect from that directory — which is how a working session starts being refused with [405](#when-the-server-answers-405-on-connect). Pin it deliberately, unset it when done. |
|
|
4471
4480
|
| `WA_STATIC_TOKEN` | Override the static token used in registration token computation. iOS only — Android has no static token. |
|
|
4472
4481
|
|
|
4473
4482
|
### When the server answers 405 on connect
|
|
4474
4483
|
|
|
4475
4484
|
```
|
|
4476
|
-
WhatsApp auth failure 405 — client outdated
|
|
4477
|
-
|
|
4485
|
+
WhatsApp auth failure 405 — client outdated. The server refused the version
|
|
4486
|
+
this connect announced, which was 2.24.10.75. That value came from WA_VERSION
|
|
4487
|
+
in the environment — the CLI also reads it out of a .env file in the directory
|
|
4488
|
+
it runs from — while the session itself holds 2.26.29.73.
|
|
4478
4489
|
```
|
|
4479
4490
|
|
|
4480
4491
|
**The session is fine and the number is still registered.** 405 is the server
|
|
4481
|
-
declining the *client*, not the account: the version being announced is one
|
|
4482
|
-
|
|
4483
|
-
|
|
4492
|
+
declining the *client*, not the account: the version being announced is not one
|
|
4493
|
+
it accepts. Registering the number again is the one move that cannot help — the
|
|
4494
|
+
same version would go out and be refused identically, at the cost of a real
|
|
4484
4495
|
phone number and a code request.
|
|
4485
4496
|
|
|
4486
|
-
|
|
4497
|
+
Connecting announces exactly one version, and there are only two places it can
|
|
4498
|
+
come from:
|
|
4499
|
+
|
|
4500
|
+
| order | where the announced version comes from |
|
|
4501
|
+
|---|---|
|
|
4502
|
+
| 1 | `WA_VERSION`, from the shell **or from a `.env` file in the directory the command runs in** |
|
|
4503
|
+
| 2 | the version stored in the session file — what the number was registered with |
|
|
4504
|
+
|
|
4505
|
+
Almost every 405 is the first line winning when nobody meant it to.
|
|
4506
|
+
|
|
4507
|
+
#### Check `WA_VERSION` before anything else
|
|
4508
|
+
|
|
4509
|
+
The CLI loads `.env` from the working directory before it does anything else, so
|
|
4510
|
+
a `WA_VERSION` left in that file is announced by *every* connect started from
|
|
4511
|
+
that directory — in place of the version the session registered with, which the
|
|
4512
|
+
server would have accepted. Nothing about the session changes, so the failure
|
|
4513
|
+
looks like a dead account and is not one.
|
|
4514
|
+
|
|
4515
|
+
```sh
|
|
4516
|
+
grep -i wa_version .env ~/.env
|
|
4517
|
+
env | grep WA_VERSION
|
|
4518
|
+
```
|
|
4519
|
+
|
|
4520
|
+
Remove or comment the line, then connect again. Since 5.12.17 you do not have to
|
|
4521
|
+
go looking: the CLI says so before it connects,
|
|
4522
|
+
|
|
4523
|
+
```
|
|
4524
|
+
warning: WA_VERSION=2.24.10.75 is pinned (shell or .env in /home/you) —
|
|
4525
|
+
connecting announces it instead of the version stored in the session.
|
|
4526
|
+
```
|
|
4527
|
+
|
|
4528
|
+
and a 405 names the version that went out and which of the two places it came
|
|
4529
|
+
from, because that decides the remedy — unset the override, or pin a newer one.
|
|
4530
|
+
|
|
4531
|
+
Pin `WA_VERSION` deliberately and temporarily, to force one specific build:
|
|
4487
4532
|
|
|
4488
4533
|
```sh
|
|
4489
4534
|
WA_VERSION=2.26.30.3 wa connect 919634847671
|
|
4490
4535
|
```
|
|
4491
4536
|
|
|
4492
|
-
|
|
4537
|
+
Leaving it in `.env` means every session on that machine announces it until the
|
|
4538
|
+
day it goes stale, wherever those sessions came from.
|
|
4493
4539
|
|
|
4494
|
-
|
|
4495
|
-
|
|
4540
|
+
#### Keeping the session's own version current
|
|
4541
|
+
|
|
4542
|
+
With no override, the session announces the version it was registered with, and
|
|
4543
|
+
each platform learns that differently:
|
|
4496
4544
|
|
|
4497
4545
|
| | how the version is found | what happens when that fails |
|
|
4498
4546
|
|---|---|---|
|
|
4499
4547
|
| iOS | looked up on the App Store | falls back to a version compiled into the library, which goes stale |
|
|
4500
|
-
| Android | read from the APK the token material came from | `wa apk-material --download` fetches the current one |
|
|
4548
|
+
| Android | read from the APK the registration token material came from | `wa apk-material --download` fetches the current one |
|
|
4501
4549
|
|
|
4502
|
-
So
|
|
4503
|
-
|
|
4504
|
-
handshake is refused. On Android the version travels with the APK, and
|
|
4505
|
-
the material refreshes the version with it:
|
|
4550
|
+
So a genuinely stale session version is mostly an iOS story: a lookup that times
|
|
4551
|
+
out or is rate limited leaves an old fallback in the session, and nothing says so
|
|
4552
|
+
until the handshake is refused. On Android the version travels with the APK, and
|
|
4553
|
+
refreshing the material refreshes the version with it:
|
|
4506
4554
|
|
|
4507
4555
|
```sh
|
|
4508
4556
|
wa apk-material --download
|
|
4509
4557
|
```
|
|
4510
4558
|
|
|
4559
|
+
#### If every Android session is refused, whatever the version
|
|
4560
|
+
|
|
4561
|
+
Then it is the library, not the version — update it. Until 5.12.15 the Android
|
|
4562
|
+
device profiles announced platform `3`, which is BlackBerry, a client WhatsApp
|
|
4563
|
+
stopped building in 2017. The server validates the announced app version
|
|
4564
|
+
*against the platform it was announced with*, so a current Android build arrived
|
|
4565
|
+
looking like an impossible BlackBerry one, and nothing in the failure named the
|
|
4566
|
+
platform. iOS announced `1` and was never affected.
|
|
4567
|
+
|
|
4568
|
+
Sessions written before the fix repair themselves the next time they are loaded
|
|
4569
|
+
— the platform is derived from the profile's `os` rather than trusted from the
|
|
4570
|
+
file — so nothing has to be registered again.
|
|
4571
|
+
|
|
4572
|
+
5.12.16 also brought the rest of the Android handshake in line with what a
|
|
4573
|
+
native Android client announces: no carrier (`mcc` and `mnc` are `000`), `en`
|
|
4574
|
+
and `US` as the locale, no `osBuildNumber`, the model rather than the model id
|
|
4575
|
+
as the device name, an uppercase `phoneId`, and `shortConnect`, `connectType`,
|
|
4576
|
+
`connectReason` and `connectAttemptCount` fixed at the values a real client
|
|
4577
|
+
sends on every connect — `connectType` had been sending `3`, which is not in the
|
|
4578
|
+
enum at all. iOS announces the real carrier and locale, which it has always been
|
|
4579
|
+
accepted with, and is unchanged.
|
|
4580
|
+
|
|
4581
|
+
### Finding out what a 405 objects to
|
|
4582
|
+
|
|
4583
|
+
When the version is right and the connect is still refused, stop guessing:
|
|
4584
|
+
`tools/diagnose-405.js` runs the login once per payload variation and prints
|
|
4585
|
+
what the server answered each time.
|
|
4586
|
+
|
|
4587
|
+
```sh
|
|
4588
|
+
node tools/diagnose-405.js 5568936182750
|
|
4589
|
+
|
|
4590
|
+
# installed globally:
|
|
4591
|
+
node $(npm root -g)/whalibmob/tools/diagnose-405.js 5568936182750
|
|
4592
|
+
```
|
|
4593
|
+
|
|
4594
|
+
```
|
|
4595
|
+
reference shape, as-is ok — LOGIN ACCEPTED
|
|
4596
|
+
+ the real carrier (mcc/mnc) ok — LOGIN ACCEPTED
|
|
4597
|
+
+ the real locale 405 {"reason":"405"}
|
|
4598
|
+
```
|
|
4599
|
+
|
|
4600
|
+
`405` means that row was refused, `401` means the client was accepted and only
|
|
4601
|
+
the credentials failed, `ok` means the login went through — so the first row
|
|
4602
|
+
that stops saying `ok` names the field the server objected to. Every row uses
|
|
4603
|
+
the session already on disk: nothing is registered, no code is requested, and
|
|
4604
|
+
the session is never written to. `--dry-run` prints the payload sizes without
|
|
4605
|
+
opening a socket.
|
|
4606
|
+
|
|
4607
|
+
If *every* row is accepted while `wa connect` is refused, the payload is not the
|
|
4608
|
+
problem and the difference is in the environment the CLI reads and the tool does
|
|
4609
|
+
not — which is `WA_VERSION`, and the top of this section.
|
|
4610
|
+
|
|
4511
4611
|
## License
|
|
4512
4612
|
|
|
4513
4613
|
MIT
|
package/cli.js
CHANGED
|
@@ -957,6 +957,17 @@ async function doConnectWeb(phone, opts) {
|
|
|
957
957
|
|
|
958
958
|
async function doConnect(phone) {
|
|
959
959
|
phone = normalizePhone(phone);
|
|
960
|
+
|
|
961
|
+
// A WA_VERSION left in .env is announced by every connect from this
|
|
962
|
+
// directory, in place of the version the session was registered with, and a
|
|
963
|
+
// stale one is refused with a 405 that says nothing about where the value
|
|
964
|
+
// came from. Say it out loud before connecting rather than after failing.
|
|
965
|
+
if (process.env.WA_VERSION) {
|
|
966
|
+
warn('WA_VERSION=' + process.env.WA_VERSION + ' is pinned (shell or .env in ' +
|
|
967
|
+
process.cwd() + ') — connecting announces it instead of the version ' +
|
|
968
|
+
'stored in the session. Unset it to use the session\'s own.');
|
|
969
|
+
}
|
|
970
|
+
|
|
960
971
|
const client = new WhalibmobClient({ sessionDir: _sessDir });
|
|
961
972
|
attachEvents(client);
|
|
962
973
|
|
package/lib/Client.js
CHANGED
|
@@ -705,6 +705,26 @@ class WhalibmobClient extends EventEmitter {
|
|
|
705
705
|
* Anything the handshake marked as a spent session stops the loop here.
|
|
706
706
|
*/
|
|
707
707
|
_onSocketError(err) {
|
|
708
|
+
// The server declined the client, not the session. The next attempt
|
|
709
|
+
// announces exactly the same thing and is refused exactly the same way, so
|
|
710
|
+
// this stops here instead of retrying once a second forever. _handleFailure
|
|
711
|
+
// already reads a 405 this way, but it only runs once a stream is open — a
|
|
712
|
+
// 405 during the handshake never reached it, which is why a refused client
|
|
713
|
+
// reconnected in a loop.
|
|
714
|
+
if (err && String(err.code) === '405') {
|
|
715
|
+
_whaDbg('[DBG] HANDSHAKE_REJECTED code=405 — client refused, not a dead session');
|
|
716
|
+
this._fatal = true;
|
|
717
|
+
this._reconnecting = false;
|
|
718
|
+
this.emit('client_rejected', {
|
|
719
|
+
reason: '405',
|
|
720
|
+
location: null,
|
|
721
|
+
message: err.message,
|
|
722
|
+
node: null
|
|
723
|
+
});
|
|
724
|
+
this.emit('error', err);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
708
728
|
if (err && err.loggedOut) {
|
|
709
729
|
_whaDbg('[DBG] HANDSHAKE_REJECTED code=' + err.code + ' — not retrying');
|
|
710
730
|
this._fatal = true;
|
package/lib/DeviceConfig.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const {
|
|
4
4
|
IOS_DEVICE,
|
|
5
5
|
IOS_DEVICE_PROFILES,
|
|
6
|
-
ANDROID_DEVICE_PROFILES
|
|
6
|
+
ANDROID_DEVICE_PROFILES,
|
|
7
|
+
PLATFORM
|
|
7
8
|
} = require('./constants');
|
|
8
9
|
|
|
9
10
|
// Pre-build normalised lookup tables so that user-supplied profile keys in any
|
|
@@ -49,7 +50,7 @@ function getDeviceConfig() {
|
|
|
49
50
|
|
|
50
51
|
return {
|
|
51
52
|
os: 'android',
|
|
52
|
-
platform:
|
|
53
|
+
platform: PLATFORM.ANDROID,
|
|
53
54
|
model: process.env.WA_DEVICE_MODEL || 'Samsung Galaxy S24 Ultra',
|
|
54
55
|
manufacturer: process.env.WA_DEVICE_MANUFACTURER || 'Samsung',
|
|
55
56
|
osVersion: process.env.WA_DEVICE_OS_VERSION || '14',
|
|
@@ -68,7 +69,7 @@ function getDeviceConfig() {
|
|
|
68
69
|
|
|
69
70
|
return {
|
|
70
71
|
os: 'ios',
|
|
71
|
-
platform:
|
|
72
|
+
platform: PLATFORM.IOS,
|
|
72
73
|
model: process.env.WA_DEVICE_MODEL || IOS_DEVICE.model,
|
|
73
74
|
manufacturer: process.env.WA_DEVICE_MANUFACTURER || IOS_DEVICE.manufacturer,
|
|
74
75
|
osVersion: process.env.WA_DEVICE_OS_VERSION || IOS_DEVICE.osVersion,
|
package/lib/Registration.js
CHANGED
|
@@ -11,7 +11,7 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
11
11
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
12
12
|
const AndroidApk = require('./AndroidApk');
|
|
13
13
|
const attestation = require('./Attestation');
|
|
14
|
-
const { dbg: _whaDbg } = require('./logger');
|
|
14
|
+
const { dbg: _whaDbg, warn: _whaWarn } = require('./logger');
|
|
15
15
|
|
|
16
16
|
// ---------- Request envelope ----------
|
|
17
17
|
//
|
|
@@ -499,12 +499,25 @@ async function fetchAndroidVersion() {
|
|
|
499
499
|
{
|
|
500
500
|
headers: {
|
|
501
501
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
502
|
-
'Accept
|
|
502
|
+
'Accept': 'text/html,application/xhtml+xml',
|
|
503
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
504
|
+
// Only what every axios build decompresses. A default that asks for
|
|
505
|
+
// brotli and then gets bytes back undecoded is a page that parses to
|
|
506
|
+
// nothing.
|
|
507
|
+
'Accept-Encoding': 'gzip, deflate'
|
|
503
508
|
},
|
|
509
|
+
responseType: 'text',
|
|
510
|
+
decompress: true,
|
|
504
511
|
timeout: 12000
|
|
505
512
|
}
|
|
506
513
|
);
|
|
507
|
-
|
|
514
|
+
// Buffer when the response was compressed and axios handed the bytes back
|
|
515
|
+
// as-is. String() on a Buffer of gzip is a page no regex will ever match,
|
|
516
|
+
// which is how this came to fall through to a two-year-old fallback without
|
|
517
|
+
// saying a word.
|
|
518
|
+
const html = Buffer.isBuffer(resp.data)
|
|
519
|
+
? resp.data.toString('utf8')
|
|
520
|
+
: String(resp.data);
|
|
508
521
|
|
|
509
522
|
// Primary: first quoted 4-part version string matching WhatsApp's "2.x.x.x" scheme.
|
|
510
523
|
// In Play Store JSON the current stable version appears first, before beta/history entries.
|
|
@@ -520,10 +533,41 @@ async function fetchAndroidVersion() {
|
|
|
520
533
|
_cachedAndroidVersion = secondary[1];
|
|
521
534
|
return secondary[1];
|
|
522
535
|
}
|
|
523
|
-
|
|
536
|
+
|
|
537
|
+
// Last resort: the highest 4-part version anywhere on the page. Play has
|
|
538
|
+
// rearranged this page before, and a version read from the wrong element
|
|
539
|
+
// still beats announcing one from two years ago.
|
|
540
|
+
const all = html.match(/2\.\d+\.\d+\.\d+/g);
|
|
541
|
+
if (all && all.length) {
|
|
542
|
+
const newest = all.sort(compareVersions).pop();
|
|
543
|
+
_whaDbg('[DBG] ANDROID_VERSION scraped from an unrecognised page layout: ' + newest);
|
|
544
|
+
_cachedAndroidVersion = newest;
|
|
545
|
+
return newest;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
_whaWarn('could not read the current WhatsApp version off the Play Store page — ' +
|
|
549
|
+
'falling back to ' + ANDROID_VERSION_FALLBACK + '. If the server refuses the ' +
|
|
550
|
+
'client with 405, set WA_VERSION to a current version.');
|
|
551
|
+
return ANDROID_VERSION_FALLBACK;
|
|
552
|
+
} catch (err) {
|
|
553
|
+
_whaWarn('could not reach the Play Store to read the current WhatsApp version (' +
|
|
554
|
+
err.message + ') — falling back to ' + ANDROID_VERSION_FALLBACK + '.');
|
|
555
|
+
}
|
|
524
556
|
return ANDROID_VERSION_FALLBACK;
|
|
525
557
|
}
|
|
526
558
|
|
|
559
|
+
// Numeric, part by part: '2.26.9.75' is older than '2.26.29.73', which a
|
|
560
|
+
// lexicographic sort gets backwards.
|
|
561
|
+
function compareVersions(a, b) {
|
|
562
|
+
const pa = String(a).split('.').map(Number);
|
|
563
|
+
const pb = String(b).split('.').map(Number);
|
|
564
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
565
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
566
|
+
if (d) return d;
|
|
567
|
+
}
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
527
571
|
// Return the appropriate WhatsApp version for the active device.
|
|
528
572
|
// If WA_VERSION is set in the environment, that value is always used.
|
|
529
573
|
//
|
package/lib/Store.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { warn: _whaWarn } = require('./logger');
|
|
3
|
+
const { warn: _whaWarn, dbg: _whaDbg } = require('./logger');
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const curveJs = require('curve25519-js');
|
|
9
9
|
const { v4: uuidv4 } = require('uuid');
|
|
10
|
-
const { IOS_DEVICE, IOS_VERSION_FALLBACK, ANDROID_VERSION_FALLBACK
|
|
10
|
+
const { IOS_DEVICE, IOS_VERSION_FALLBACK, ANDROID_VERSION_FALLBACK,
|
|
11
|
+
platformForOs } = require('./constants');
|
|
11
12
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
12
13
|
|
|
13
14
|
// ─────────────────────────────────────────────────────────
|
|
@@ -101,6 +102,25 @@ function createNewStore(phoneNumber) {
|
|
|
101
102
|
// Serialisation / deserialisation
|
|
102
103
|
// ─────────────────────────────────────────────────────────
|
|
103
104
|
|
|
105
|
+
// The device profile as it should be announced, whatever a session file happens
|
|
106
|
+
// to hold.
|
|
107
|
+
//
|
|
108
|
+
// `platform` is the number that goes into the handshake, and Android sessions
|
|
109
|
+
// written before it was corrected carry 3 — BlackBerry — which the server
|
|
110
|
+
// refuses with 405 on every connect. `os` is the part that was ever chosen
|
|
111
|
+
// deliberately, so it decides, and a stale number is repaired the moment the
|
|
112
|
+
// session is read. Nothing has to be registered again over it.
|
|
113
|
+
function normaliseDevice(device) {
|
|
114
|
+
const merged = Object.assign({}, IOS_DEVICE, device || null);
|
|
115
|
+
const expected = platformForOs(merged.os);
|
|
116
|
+
if (merged.platform !== expected) {
|
|
117
|
+
_whaDbg('[DBG] DEVICE_PLATFORM_FIXED os=' + merged.os +
|
|
118
|
+
' ' + merged.platform + ' → ' + expected);
|
|
119
|
+
merged.platform = expected;
|
|
120
|
+
}
|
|
121
|
+
return merged;
|
|
122
|
+
}
|
|
123
|
+
|
|
104
124
|
function storeToJson(store) {
|
|
105
125
|
if (!store) {
|
|
106
126
|
throw new Error('storeToJson: store is undefined or null');
|
|
@@ -112,7 +132,7 @@ function storeToJson(store) {
|
|
|
112
132
|
|
|
113
133
|
const name = store.name || 'User';
|
|
114
134
|
const version = store.version || IOS_VERSION_FALLBACK;
|
|
115
|
-
const device =
|
|
135
|
+
const device = normaliseDevice(store.device);
|
|
116
136
|
|
|
117
137
|
const advIdentity = store.advIdentity
|
|
118
138
|
? store.advIdentity.toString('base64')
|
|
@@ -156,7 +176,7 @@ function storeFromJson(obj) {
|
|
|
156
176
|
|
|
157
177
|
const name = obj.name || 'User';
|
|
158
178
|
const version = obj.version || IOS_VERSION_FALLBACK;
|
|
159
|
-
const device =
|
|
179
|
+
const device = normaliseDevice(obj.device);
|
|
160
180
|
|
|
161
181
|
return {
|
|
162
182
|
phoneNumber: obj.phoneNumber,
|
package/lib/constants.js
CHANGED
|
@@ -42,17 +42,54 @@ const IOS_STATIC_TOKEN = '0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM';
|
|
|
42
42
|
const IOS_BUSINESS_STATIC_TOKEN = 'USUDuDYDeQhY4RF2fCSp5m3F6kJ1M2J8wS7bbNA2';
|
|
43
43
|
|
|
44
44
|
// WhatsApp version fallbacks — used when live fetch fails.
|
|
45
|
+
//
|
|
46
|
+
// These are announced to the server, which refuses a build it does not
|
|
47
|
+
// recognise, so a fallback that has gone stale is a connection that fails for a
|
|
48
|
+
// reason nothing in the error names. Keep them within a few releases of what
|
|
49
|
+
// the stores are actually serving; the live lookup is what normally decides.
|
|
45
50
|
const IOS_VERSION_FALLBACK = '2.26.9.75';
|
|
46
|
-
const ANDROID_VERSION_FALLBACK = '2.
|
|
51
|
+
const ANDROID_VERSION_FALLBACK = '2.26.29.73';
|
|
47
52
|
|
|
48
53
|
// Safari UA for the iTunes lookup only.
|
|
49
54
|
const IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1';
|
|
50
55
|
|
|
56
|
+
// ─── ClientPayload.UserAgent.Platform ────────────────────────────────────────
|
|
57
|
+
//
|
|
58
|
+
// The enum the handshake announces the client with, straight off the schema:
|
|
59
|
+
//
|
|
60
|
+
// ANDROID = 0 IOS = 1 WINDOWS_PHONE = 2 BLACKBERRY = 3 ... WEB = 14
|
|
61
|
+
//
|
|
62
|
+
// The server validates the announced app version *against this platform*, so a
|
|
63
|
+
// wrong value here does not read as a wrong platform — it reads as an
|
|
64
|
+
// impossible version and comes back as <failure reason="405"> ("client
|
|
65
|
+
// outdated") before a single stanza is exchanged. Android sat on 3 (BlackBerry,
|
|
66
|
+
// a client WhatsApp stopped building in 2017) and every Android session was
|
|
67
|
+
// refused on connect with a version that was two weeks old, while iOS on 1 was
|
|
68
|
+
// correct and never saw a 405. Nothing in the failure says which field was
|
|
69
|
+
// wrong, which is what made it worth this many words.
|
|
70
|
+
//
|
|
71
|
+
// Anything that needs a platform number takes it from here.
|
|
72
|
+
const PLATFORM = {
|
|
73
|
+
ANDROID: 0,
|
|
74
|
+
IOS: 1,
|
|
75
|
+
WEB: 14
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// The platform an `os` string announces itself as. Unknown values fall back to
|
|
79
|
+
// iOS, which is what a session with no device profile has always been.
|
|
80
|
+
function platformForOs(os) {
|
|
81
|
+
return String(os).toLowerCase() === 'android' ? PLATFORM.ANDROID : PLATFORM.IOS;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A note on the `deviceModelType` numbers in the profiles below: nothing reads
|
|
85
|
+
// them. The proto's deviceModelType (field 16) is a string and is filled from
|
|
86
|
+
// `modelId`; the proto's deviceType (field 15) is the DeviceType enum, which is
|
|
87
|
+
// PHONE for every profile here. The numbers are left as they were found.
|
|
88
|
+
|
|
51
89
|
// Default iOS device (iPhone 15 Pro, iOS 17.4.1).
|
|
52
|
-
// platform 1 = iOS in WhatsApp's ClientPayload.UserAgent proto enum.
|
|
53
90
|
const IOS_DEVICE = {
|
|
54
91
|
os: 'ios',
|
|
55
|
-
platform:
|
|
92
|
+
platform: PLATFORM.IOS,
|
|
56
93
|
model: 'iPhone 15 Pro',
|
|
57
94
|
manufacturer: 'Apple',
|
|
58
95
|
osVersion: '17.4.1',
|
|
@@ -65,37 +102,37 @@ const IOS_DEVICE = {
|
|
|
65
102
|
// Set WA_OS=ios WA_DEVICE=<key> in .env to select one.
|
|
66
103
|
const IOS_DEVICE_PROFILES = {
|
|
67
104
|
'iphone15pro': {
|
|
68
|
-
os: 'ios', platform:
|
|
105
|
+
os: 'ios', platform: PLATFORM.IOS,
|
|
69
106
|
model: 'iPhone 15 Pro', manufacturer: 'Apple',
|
|
70
107
|
osVersion: '17.4.1', osBuildNumber: '21E236',
|
|
71
108
|
modelId: 'iPhone16,1', deviceModelType: 2
|
|
72
109
|
},
|
|
73
110
|
'iphone15': {
|
|
74
|
-
os: 'ios', platform:
|
|
111
|
+
os: 'ios', platform: PLATFORM.IOS,
|
|
75
112
|
model: 'iPhone 15', manufacturer: 'Apple',
|
|
76
113
|
osVersion: '17.4.1', osBuildNumber: '21E236',
|
|
77
114
|
modelId: 'iPhone15,4', deviceModelType: 2
|
|
78
115
|
},
|
|
79
116
|
'iphone14': {
|
|
80
|
-
os: 'ios', platform:
|
|
117
|
+
os: 'ios', platform: PLATFORM.IOS,
|
|
81
118
|
model: 'iPhone 14', manufacturer: 'Apple',
|
|
82
119
|
osVersion: '17.4.1', osBuildNumber: '21E236',
|
|
83
120
|
modelId: 'iPhone14,2', deviceModelType: 2
|
|
84
121
|
},
|
|
85
122
|
'iphone14pro': {
|
|
86
|
-
os: 'ios', platform:
|
|
123
|
+
os: 'ios', platform: PLATFORM.IOS,
|
|
87
124
|
model: 'iPhone 14 Pro', manufacturer: 'Apple',
|
|
88
125
|
osVersion: '17.4.1', osBuildNumber: '21E236',
|
|
89
126
|
modelId: 'iPhone15,2', deviceModelType: 2
|
|
90
127
|
},
|
|
91
128
|
'iphone13': {
|
|
92
|
-
os: 'ios', platform:
|
|
129
|
+
os: 'ios', platform: PLATFORM.IOS,
|
|
93
130
|
model: 'iPhone 13', manufacturer: 'Apple',
|
|
94
131
|
osVersion: '16.7.8', osBuildNumber: '20H343',
|
|
95
132
|
modelId: 'iPhone14,5', deviceModelType: 2
|
|
96
133
|
},
|
|
97
134
|
'iphone12': {
|
|
98
|
-
os: 'ios', platform:
|
|
135
|
+
os: 'ios', platform: PLATFORM.IOS,
|
|
99
136
|
model: 'iPhone 12', manufacturer: 'Apple',
|
|
100
137
|
osVersion: '15.8.3', osBuildNumber: '19H384',
|
|
101
138
|
modelId: 'iPhone13,2', deviceModelType: 1
|
|
@@ -104,94 +141,93 @@ const IOS_DEVICE_PROFILES = {
|
|
|
104
141
|
|
|
105
142
|
// ─── Android device profiles ───────────────────────────────────────────────
|
|
106
143
|
// Set WA_OS=android WA_DEVICE=<key> in .env to select one.
|
|
107
|
-
// platform 3 = ANDROID in WhatsApp's ClientPayload.UserAgent proto enum.
|
|
108
144
|
const ANDROID_DEVICE_PROFILES = {
|
|
109
145
|
'samsung-s24-ultra': {
|
|
110
|
-
os: 'android', platform:
|
|
146
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
111
147
|
model: 'Samsung Galaxy S24 Ultra', manufacturer: 'Samsung',
|
|
112
148
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
113
149
|
modelId: 'SM-S928B', deviceModelType: 2
|
|
114
150
|
},
|
|
115
151
|
'samsung-s24': {
|
|
116
|
-
os: 'android', platform:
|
|
152
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
117
153
|
model: 'Samsung Galaxy S24', manufacturer: 'Samsung',
|
|
118
154
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
119
155
|
modelId: 'SM-S921B', deviceModelType: 2
|
|
120
156
|
},
|
|
121
157
|
'samsung-s23': {
|
|
122
|
-
os: 'android', platform:
|
|
158
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
123
159
|
model: 'Samsung Galaxy S23', manufacturer: 'Samsung',
|
|
124
160
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
125
161
|
modelId: 'SM-S911B', deviceModelType: 2
|
|
126
162
|
},
|
|
127
163
|
'samsung-s23-ultra': {
|
|
128
|
-
os: 'android', platform:
|
|
164
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
129
165
|
model: 'Samsung Galaxy S23 Ultra', manufacturer: 'Samsung',
|
|
130
166
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
131
167
|
modelId: 'SM-S918B', deviceModelType: 2
|
|
132
168
|
},
|
|
133
169
|
'samsung-a55': {
|
|
134
|
-
os: 'android', platform:
|
|
170
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
135
171
|
model: 'Samsung Galaxy A55', manufacturer: 'Samsung',
|
|
136
172
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
137
173
|
modelId: 'SM-A556B', deviceModelType: 1
|
|
138
174
|
},
|
|
139
175
|
'pixel8pro': {
|
|
140
|
-
os: 'android', platform:
|
|
176
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
141
177
|
model: 'Pixel 8 Pro', manufacturer: 'Google',
|
|
142
178
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
143
179
|
modelId: 'Pixel 8 Pro', deviceModelType: 2
|
|
144
180
|
},
|
|
145
181
|
'pixel8': {
|
|
146
|
-
os: 'android', platform:
|
|
182
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
147
183
|
model: 'Pixel 8', manufacturer: 'Google',
|
|
148
184
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
149
185
|
modelId: 'Pixel 8', deviceModelType: 2
|
|
150
186
|
},
|
|
151
187
|
'pixel7': {
|
|
152
|
-
os: 'android', platform:
|
|
188
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
153
189
|
model: 'Pixel 7', manufacturer: 'Google',
|
|
154
190
|
osVersion: '14', osBuildNumber: 'TP1A.221005.002',
|
|
155
191
|
modelId: 'Pixel 7', deviceModelType: 2
|
|
156
192
|
},
|
|
157
193
|
'pixel7a': {
|
|
158
|
-
os: 'android', platform:
|
|
194
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
159
195
|
model: 'Pixel 7a', manufacturer: 'Google',
|
|
160
196
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
161
197
|
modelId: 'Pixel 7a', deviceModelType: 1
|
|
162
198
|
},
|
|
163
199
|
'xiaomi14': {
|
|
164
|
-
os: 'android', platform:
|
|
200
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
165
201
|
model: 'Xiaomi 14', manufacturer: 'Xiaomi',
|
|
166
202
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
167
203
|
modelId: '23127PN0CG', deviceModelType: 2
|
|
168
204
|
},
|
|
169
205
|
'xiaomi13': {
|
|
170
|
-
os: 'android', platform:
|
|
206
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
171
207
|
model: 'Xiaomi 13', manufacturer: 'Xiaomi',
|
|
172
208
|
osVersion: '13', osBuildNumber: 'TQ3A.230901.001',
|
|
173
209
|
modelId: '2211133G', deviceModelType: 2
|
|
174
210
|
},
|
|
175
211
|
'oneplus12': {
|
|
176
|
-
os: 'android', platform:
|
|
212
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
177
213
|
model: 'OnePlus 12', manufacturer: 'OnePlus',
|
|
178
214
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
179
215
|
modelId: 'CPH2573', deviceModelType: 2
|
|
180
216
|
},
|
|
181
217
|
'oneplus11': {
|
|
182
|
-
os: 'android', platform:
|
|
218
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
183
219
|
model: 'OnePlus 11', manufacturer: 'OnePlus',
|
|
184
220
|
osVersion: '13', osBuildNumber: 'TQ3A.230901.001',
|
|
185
221
|
modelId: 'CPH2449', deviceModelType: 2
|
|
186
222
|
},
|
|
187
223
|
'oppo-find-x7': {
|
|
188
|
-
os: 'android', platform:
|
|
224
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
189
225
|
model: 'OPPO Find X7', manufacturer: 'OPPO',
|
|
190
226
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
191
227
|
modelId: 'CPH2599', deviceModelType: 2
|
|
192
228
|
},
|
|
193
229
|
'realme-gt5': {
|
|
194
|
-
os: 'android', platform:
|
|
230
|
+
os: 'android', platform: PLATFORM.ANDROID,
|
|
195
231
|
model: 'realme GT 5 Pro', manufacturer: 'realme',
|
|
196
232
|
osVersion: '14', osBuildNumber: 'UP1A.231005.007',
|
|
197
233
|
modelId: 'RMX3888', deviceModelType: 2
|
|
@@ -235,6 +271,8 @@ module.exports = {
|
|
|
235
271
|
IOS_VERSION_FALLBACK,
|
|
236
272
|
ANDROID_VERSION_FALLBACK,
|
|
237
273
|
IOS_USER_AGENT,
|
|
274
|
+
PLATFORM,
|
|
275
|
+
platformForOs,
|
|
238
276
|
IOS_DEVICE,
|
|
239
277
|
IOS_DEVICE_PROFILES,
|
|
240
278
|
ANDROID_DEVICE_PROFILES,
|
package/lib/noise.js
CHANGED
|
@@ -6,9 +6,11 @@ const EventEmitter = require('events');
|
|
|
6
6
|
const curveJs = require('curve25519-js');
|
|
7
7
|
const { sha256 } = require('@noble/hashes/sha256');
|
|
8
8
|
const { hkdf } = require('@noble/hashes/hkdf');
|
|
9
|
-
const { MOBILE_PROLOGUE, WEB_PROLOGUE, WHATSAPP_HOST, WHATSAPP_PORT
|
|
9
|
+
const { MOBILE_PROLOGUE, WEB_PROLOGUE, WHATSAPP_HOST, WHATSAPP_PORT,
|
|
10
|
+
platformForOs } = require('./constants');
|
|
10
11
|
const { encodeHandshakeClientHello, encodeHandshakeClientFinish,
|
|
11
12
|
decodeServerHello, encodeClientPayload } = require('./proto');
|
|
13
|
+
const { dbg: _whaDbg } = require('./logger');
|
|
12
14
|
const { parsePhone, getCountryMeta } = require('./Registration');
|
|
13
15
|
|
|
14
16
|
// ─── CertChain validator ────────
|
|
@@ -66,6 +68,16 @@ function validateServerCert(payloadBuf) {
|
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// ─── ClientPayload enum values ───────────────────────────────────────────────
|
|
72
|
+
//
|
|
73
|
+
// ClientPayload.ConnectType, ClientPayload.ConnectReason and
|
|
74
|
+
// ClientPayload.UserAgent.DeviceType. Named here because a bare number in the
|
|
75
|
+
// payload is exactly how this file came to announce a connect type that is not
|
|
76
|
+
// in the enum at all.
|
|
77
|
+
const CONNECT_TYPE_WIFI = 1; // WIFI_UNKNOWN
|
|
78
|
+
const CONNECT_REASON_USER_ACTIVATED = 1;
|
|
79
|
+
const DEVICE_TYPE_PHONE = 0;
|
|
80
|
+
|
|
69
81
|
// ─── Curve25519 DH helpers ───────────────────────────────────────────────────
|
|
70
82
|
|
|
71
83
|
function stripKeyPrefix(pub) {
|
|
@@ -362,37 +374,79 @@ class NoiseSocket extends EventEmitter {
|
|
|
362
374
|
// Auto-derive MCC/MNC and locale from the registered phone number so the
|
|
363
375
|
// ClientPayload userAgent is never sent with the telltale '000'/'000' values.
|
|
364
376
|
const _phoneMeta = getCountryMeta(parsePhone(this.store.phoneNumber).cc);
|
|
377
|
+
const _device = this.store.device || {};
|
|
378
|
+
const _isAndroid = String(_device.os).toLowerCase() === 'android';
|
|
379
|
+
const _version = process.env.WA_VERSION || this.store.version;
|
|
380
|
+
|
|
381
|
+
this._announcedVersion = _version;
|
|
382
|
+
this._versionFromEnv = !!process.env.WA_VERSION;
|
|
383
|
+
this._sessionVersion = this.store.version;
|
|
384
|
+
_whaDbg('[DBG] ANNOUNCING version=' + _version +
|
|
385
|
+
(this._versionFromEnv ? ' (from WA_VERSION, session holds ' +
|
|
386
|
+
this.store.version + ')' : ' (from the session)') +
|
|
387
|
+
' platform=' + platformForOs(_device.os));
|
|
365
388
|
|
|
366
389
|
const payload = encodeClientPayload({
|
|
367
390
|
username: BigInt(this.store.phoneNumber),
|
|
368
391
|
passive: false,
|
|
369
392
|
pushName: this.store.registered ? (this.store.name || null) : null,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
393
|
+
// The next four are constants in the reference client: it announces
|
|
394
|
+
// shortConnect, WIFI_UNKNOWN, USER_ACTIVATED and attempt 0 on every
|
|
395
|
+
// connect, retries included. ConnectType is a closed set — 0
|
|
396
|
+
// CELLULAR_UNKNOWN, 1 WIFI_UNKNOWN, 100-112 for the named cellular
|
|
397
|
+
// radios — and a reconnect used to send 3, which is not in it.
|
|
398
|
+
shortConnect: true,
|
|
399
|
+
connectType: CONNECT_TYPE_WIFI,
|
|
400
|
+
connectReason: CONNECT_REASON_USER_ACTIVATED,
|
|
401
|
+
connectAttemptCount: 0,
|
|
374
402
|
device: 0,
|
|
375
403
|
oc: false,
|
|
376
404
|
userAgent: {
|
|
377
|
-
|
|
405
|
+
// Which client this says it is. The server checks the announced
|
|
406
|
+
// version against *this* platform, so a wrong number here comes back
|
|
407
|
+
// as 405 "client outdated" and never mentions the platform at all —
|
|
408
|
+
// see PLATFORM in constants.js. Derived from `os` rather than read
|
|
409
|
+
// out of the session, so a session file written before this was
|
|
410
|
+
// corrected cannot keep announcing BlackBerry. ANDROID is 0, so the
|
|
411
|
+
// old `|| 1` fallback would also have quietly turned every Android
|
|
412
|
+
// session into an iOS one.
|
|
413
|
+
platform: platformForOs(_device.os),
|
|
378
414
|
// The version the session was registered with, unless WA_VERSION says
|
|
379
415
|
// otherwise. Without that override there is no way out of a 405: the
|
|
380
416
|
// version lives in the session file, the server refuses it, and the
|
|
381
417
|
// only remaining move is to register the number again — which is the
|
|
382
418
|
// one thing that should never be the answer to a client-side problem.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
419
|
+
//
|
|
420
|
+
// The override is also a way *into* a 405, and a quiet one: the CLI
|
|
421
|
+
// reads .env before anything else, so a WA_VERSION left in that file
|
|
422
|
+
// is announced by every connect from that directory while the session
|
|
423
|
+
// file still holds a version the server would have accepted. Both are
|
|
424
|
+
// remembered here so the failure can say which one went out.
|
|
425
|
+
version: _version,
|
|
426
|
+
// Android announces no carrier and no locale of its own: the
|
|
427
|
+
// reference client sends 000/000 and en/US on this platform and is
|
|
428
|
+
// accepted, and this is the half that was being refused. iOS is
|
|
429
|
+
// accepted as it is and keeps sending the real values.
|
|
430
|
+
mcc: _isAndroid ? '000' : _phoneMeta.mcc,
|
|
431
|
+
mnc: _isAndroid ? '000' : _phoneMeta.mnc,
|
|
432
|
+
osVersion: _device.osVersion,
|
|
433
|
+
manufacturer: _device.manufacturer,
|
|
434
|
+
device: _device.model,
|
|
435
|
+
// Not sent on Android by the reference client, which describes the
|
|
436
|
+
// handset with manufacturer and model alone. iOS does send it.
|
|
437
|
+
osBuildNumber: _isAndroid ? null : _device.osBuildNumber,
|
|
438
|
+
// Uppercase on both platforms, even though the Android registration
|
|
439
|
+
// sends the same id lowercase — that asymmetry is the reference
|
|
440
|
+
// client's, not an oversight.
|
|
390
441
|
phoneId: this.store.fdid.toUpperCase(),
|
|
391
442
|
releaseChannel: 0,
|
|
392
|
-
localeLanguage: _phoneMeta.lg,
|
|
393
|
-
localeCountry: _phoneMeta.lc,
|
|
394
|
-
|
|
395
|
-
deviceModelType
|
|
443
|
+
localeLanguage: _isAndroid ? 'en' : _phoneMeta.lg,
|
|
444
|
+
localeCountry: _isAndroid ? 'US' : _phoneMeta.lc,
|
|
445
|
+
// DeviceType enum (field 15): PHONE. Every profile shipped here is a
|
|
446
|
+
// phone — the numeric `deviceModelType` on the profiles is unrelated
|
|
447
|
+
// to this and is read by nothing.
|
|
448
|
+
deviceType: DEVICE_TYPE_PHONE,
|
|
449
|
+
deviceModelType: _device.modelId
|
|
396
450
|
}
|
|
397
451
|
});
|
|
398
452
|
|
|
@@ -476,12 +530,12 @@ class NoiseSocket extends EventEmitter {
|
|
|
476
530
|
// and refused identically. _handleFailure already reads it this way
|
|
477
531
|
// once a session is up; this is the same code arriving during the
|
|
478
532
|
// handshake, and it was the one path still calling it revoked.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
533
|
+
// Filled in below, where the version that actually went out is
|
|
534
|
+
// known. Telling everyone to set WA_VERSION was wrong exactly when
|
|
535
|
+
// it mattered most: a stale WA_VERSION is itself a way to get a 405,
|
|
536
|
+
// and the advice then read as "fix it by doing more of the thing
|
|
537
|
+
// that broke it".
|
|
538
|
+
'405': { wipe: false, text: 'client outdated.' },
|
|
485
539
|
'406': { wipe: true, text: 'banned.' },
|
|
486
540
|
'409': { wipe: true, text: 'bad user agent — the client identified ' +
|
|
487
541
|
'itself in a way the server rejects.' },
|
|
@@ -492,8 +546,32 @@ class NoiseSocket extends EventEmitter {
|
|
|
492
546
|
'503': { wipe: false, text: 'service unavailable — worth retrying.' }
|
|
493
547
|
};
|
|
494
548
|
const info = FAILURE[reason];
|
|
549
|
+
let detail = info ? info.text : '';
|
|
550
|
+
|
|
551
|
+
// A refused client is a refused *version*, so name the one that went
|
|
552
|
+
// out and say where it came from. Which of the two it was decides the
|
|
553
|
+
// whole remedy, and the failure itself says neither.
|
|
554
|
+
// The companion path builds its own payload and never sets these, so
|
|
555
|
+
// it gets the short form rather than a sentence about undefined.
|
|
556
|
+
if (reason === '405' && !this._announcedVersion) {
|
|
557
|
+
detail += ' The server refused the version this connect announced.';
|
|
558
|
+
} else if (reason === '405') {
|
|
559
|
+
detail += ' The server refused the version this connect announced, ' +
|
|
560
|
+
'which was ' + this._announcedVersion + '.';
|
|
561
|
+
detail += this._versionFromEnv
|
|
562
|
+
? ' That value came from WA_VERSION in the environment — the CLI ' +
|
|
563
|
+
'also reads it out of a .env file in the directory it runs from ' +
|
|
564
|
+
'— while the session itself holds ' + this._sessionVersion +
|
|
565
|
+
'. Unset WA_VERSION to announce the session\'s own version again.'
|
|
566
|
+
: ' It came from the session file. Announce a current one with ' +
|
|
567
|
+
'WA_VERSION, or on Android re-read it from the store with ' +
|
|
568
|
+
'`wa apk-material --download`.';
|
|
569
|
+
detail += ' Nothing about the session is wrong and there is nothing ' +
|
|
570
|
+
'to re-register.';
|
|
571
|
+
}
|
|
572
|
+
|
|
495
573
|
const err = new Error('WhatsApp auth failure ' + reason +
|
|
496
|
-
(
|
|
574
|
+
(detail ? ' — ' + detail : ''));
|
|
497
575
|
err.code = reason;
|
|
498
576
|
// True when the session is spent and the caller should register or
|
|
499
577
|
// link again rather than retry.
|
package/lib/webproto.js
CHANGED
|
@@ -137,8 +137,11 @@ function encodeDeviceProps(opts) {
|
|
|
137
137
|
// device id, no phone id. That absence is part of what marks the connection as
|
|
138
138
|
// a companion rather than a primary.
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// From the one table that holds them, so the mobile and web halves can never
|
|
141
|
+
// drift apart on what a platform number means again.
|
|
142
|
+
const { PLATFORM } = require('./constants');
|
|
143
|
+
const PLATFORM_WEB = PLATFORM.WEB;
|
|
144
|
+
const PLATFORM_ANDROID = PLATFORM.ANDROID;
|
|
142
145
|
|
|
143
146
|
function encodeAppVersion(v) {
|
|
144
147
|
return Buffer.concat([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.12.
|
|
3
|
+
"version": "5.12.17",
|
|
4
4
|
"description": "WhatsApp library for interaction with WhatsApp Mobile API and web ",
|
|
5
5
|
"author": "Kunboruto20",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"index.js",
|
|
12
12
|
"cli.js",
|
|
13
13
|
"lib/**/*.js",
|
|
14
|
+
"tools/**/*.js",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE",
|
|
16
17
|
".env.example"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// Asks the server which part of the login it objects to.
|
|
5
|
+
//
|
|
6
|
+
// node tools/diagnose-405.js <phone> [--session <dir>]
|
|
7
|
+
//
|
|
8
|
+
// A 405 says "client outdated" and names nothing else, so the only way to find
|
|
9
|
+
// out what it actually dislikes is to vary one thing at a time and read the
|
|
10
|
+
// answer. Each row below is one login attempt with the session already on disk:
|
|
11
|
+
// nothing is registered, no code is requested, and the session is never
|
|
12
|
+
// written to. The verdict per row is what the server replied.
|
|
13
|
+
//
|
|
14
|
+
// 405 the client was refused — that row's combination is not accepted
|
|
15
|
+
// 401 the client was accepted and only the credentials were rejected
|
|
16
|
+
// ok the login succeeded
|
|
17
|
+
//
|
|
18
|
+
// Read it by comparing rows: the first row that stops saying 405 names the
|
|
19
|
+
// field that was the problem.
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
|
|
24
|
+
const LIB = path.join(__dirname, '..', 'lib');
|
|
25
|
+
const { loadStore } = require(path.join(LIB, 'Store.js'));
|
|
26
|
+
const { encodeClientPayload } = require(path.join(LIB, 'proto.js'));
|
|
27
|
+
const { NoiseSocket } = require(path.join(LIB, 'noise.js'));
|
|
28
|
+
const { parsePhone, getCountryMeta } = require(path.join(LIB, 'Registration.js'));
|
|
29
|
+
const { platformForOs, PLATFORM } = require(path.join(LIB, 'constants.js'));
|
|
30
|
+
|
|
31
|
+
// Keep the whole <failure> node, not just the reason the library keeps.
|
|
32
|
+
const BinaryNode = require(path.join(LIB, 'BinaryNode.js'));
|
|
33
|
+
const decodeNode = BinaryNode.decodeNode;
|
|
34
|
+
let lastNode = null;
|
|
35
|
+
BinaryNode.decodeNode = function () {
|
|
36
|
+
lastNode = decodeNode.apply(this, arguments);
|
|
37
|
+
return lastNode;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const phone = String(args[0] || '').replace(/\D/g, '');
|
|
42
|
+
const sessArg = args.indexOf('--session');
|
|
43
|
+
const sessDir = sessArg !== -1 ? args[sessArg + 1] : path.join(os.homedir(), '.waSession');
|
|
44
|
+
|
|
45
|
+
if (!phone) {
|
|
46
|
+
console.error('usage: node tools/diagnose-405.js <phone> [--session <dir>]');
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const store = loadStore(path.join(sessDir, phone + '.json'));
|
|
51
|
+
if (!store) {
|
|
52
|
+
console.error('no session for ' + phone + ' in ' + sessDir);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const meta = getCountryMeta(parsePhone(store.phoneNumber).cc);
|
|
57
|
+
const device = store.device || {};
|
|
58
|
+
const isAndroid = String(device.os).toLowerCase() === 'android';
|
|
59
|
+
|
|
60
|
+
// What the reference client announces on Android, and the knobs that differ
|
|
61
|
+
// between it and what this library used to send.
|
|
62
|
+
function payloadFor(over) {
|
|
63
|
+
over = over || {};
|
|
64
|
+
const ua = {
|
|
65
|
+
platform: over.platform !== undefined ? over.platform : platformForOs(device.os),
|
|
66
|
+
version: over.version || store.version,
|
|
67
|
+
mcc: over.realCarrier ? meta.mcc : '000',
|
|
68
|
+
mnc: over.realCarrier ? meta.mnc : '000',
|
|
69
|
+
osVersion: device.osVersion,
|
|
70
|
+
manufacturer: device.manufacturer,
|
|
71
|
+
device: over.useModelId ? device.modelId : device.model,
|
|
72
|
+
osBuildNumber: over.withBuild ? device.osBuildNumber : null,
|
|
73
|
+
phoneId: over.lowerPhoneId ? store.fdid.toLowerCase() : store.fdid.toUpperCase(),
|
|
74
|
+
releaseChannel: 0,
|
|
75
|
+
localeLanguage: over.realLocale ? meta.lg : 'en',
|
|
76
|
+
localeCountry: over.realLocale ? meta.lc : 'US',
|
|
77
|
+
deviceType: 0,
|
|
78
|
+
deviceModelType: device.modelId
|
|
79
|
+
};
|
|
80
|
+
return encodeClientPayload({
|
|
81
|
+
username: BigInt(store.phoneNumber),
|
|
82
|
+
passive: false,
|
|
83
|
+
pushName: store.registered ? (store.name || null) : null,
|
|
84
|
+
shortConnect: true,
|
|
85
|
+
connectType: 1,
|
|
86
|
+
connectReason: 1,
|
|
87
|
+
connectAttemptCount: 0,
|
|
88
|
+
device: 0,
|
|
89
|
+
oc: false,
|
|
90
|
+
userAgent: ua
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function attempt(over) {
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
lastNode = null;
|
|
97
|
+
const sock = new NoiseSocket(store, { buildPayload: () => payloadFor(over) });
|
|
98
|
+
let settled = false;
|
|
99
|
+
const finish = (verdict) => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
try { sock.close(); } catch (_) {}
|
|
104
|
+
const attrs = lastNode && lastNode.description === 'failure' && lastNode.attrs;
|
|
105
|
+
resolve(verdict + (attrs ? ' ' + JSON.stringify(attrs) : ''));
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(() => finish('timeout'), 30000);
|
|
108
|
+
sock.on('open', () => finish('ok — LOGIN ACCEPTED'));
|
|
109
|
+
sock.on('error', e => finish(e.code ? String(e.code) : e.message));
|
|
110
|
+
sock.connect().catch(() => {});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
115
|
+
|
|
116
|
+
// One variable at a time, starting from the reference client's exact shape.
|
|
117
|
+
const ROWS = [
|
|
118
|
+
['reference client, as-is', {}],
|
|
119
|
+
[' + the real carrier (mcc/mnc)', { realCarrier: true }],
|
|
120
|
+
[' + the real locale', { realLocale: true }],
|
|
121
|
+
[' + osBuildNumber', { withBuild: true }],
|
|
122
|
+
[' + model id as the device', { useModelId: true }],
|
|
123
|
+
[' + lowercase phoneId', { lowerPhoneId: true }],
|
|
124
|
+
['everything the old code sent', { realCarrier: true, realLocale: true, withBuild: true }],
|
|
125
|
+
['announced as iOS', { platform: PLATFORM.IOS }]
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// --dry-run builds every row's payload and prints its size without opening a
|
|
129
|
+
// socket, so the tool itself can be checked without talking to the server.
|
|
130
|
+
if (args.includes('--dry-run')) {
|
|
131
|
+
console.log('session +' + store.phoneNumber + ' (dry run — nothing is sent)\n');
|
|
132
|
+
for (const [label, over] of ROWS) {
|
|
133
|
+
console.log(label.padEnd(34) + payloadFor(over).length + ' bytes');
|
|
134
|
+
}
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
(async () => {
|
|
139
|
+
console.log('session +' + store.phoneNumber);
|
|
140
|
+
console.log('platform ' + (isAndroid ? 'android' : 'ios') + ' (' + platformForOs(device.os) + ')');
|
|
141
|
+
console.log('version ' + store.version);
|
|
142
|
+
console.log('device ' + device.manufacturer + ' / ' + device.model + ' / ' + device.modelId);
|
|
143
|
+
console.log('build ' + (device.osBuildNumber || '—'));
|
|
144
|
+
console.log('');
|
|
145
|
+
for (const [label, over] of ROWS) {
|
|
146
|
+
process.stdout.write(label.padEnd(34));
|
|
147
|
+
const verdict = await attempt(over);
|
|
148
|
+
console.log(verdict);
|
|
149
|
+
await sleep(4000);
|
|
150
|
+
}
|
|
151
|
+
console.log('\nthe first row that is not 405 names what the server objected to.');
|
|
152
|
+
process.exit(0);
|
|
153
|
+
})();
|