whalibmob 5.12.15 → 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/.env.example +6 -4
- package/README.md +212 -7
- package/cli.js +11 -0
- package/lib/Client.js +20 -0
- package/lib/DeviceConfig.js +4 -3
- package/lib/Registration.js +84 -15
- package/lib/Store.js +24 -4
- package/lib/constants.js +63 -25
- package/lib/noise.js +115 -20
- package/lib/webproto.js +5 -2
- package/package.json +2 -1
- package/tools/diagnose-405.js +153 -0
package/.env.example
CHANGED
|
@@ -52,11 +52,13 @@
|
|
|
52
52
|
|
|
53
53
|
# ─── WhatsApp version ────────────────────────────────────────────────────────
|
|
54
54
|
# Pin the WhatsApp client version. When set, the live version lookup is skipped
|
|
55
|
-
# entirely
|
|
55
|
+
# entirely, and this is what is announced on connect in place of the version
|
|
56
|
+
# stored in the session. Leave unset to fetch the current version automatically.
|
|
56
57
|
#
|
|
57
|
-
# On Android this is normally not needed
|
|
58
|
-
#
|
|
59
|
-
#
|
|
58
|
+
# On Android this is normally not needed: the version comes from the APK the
|
|
59
|
+
# token material was read from. On iOS it is looked up on the App Store, and a
|
|
60
|
+
# lookup that fails leaves a built-in fallback version in the session — which
|
|
61
|
+
# the server refuses at connect with a 405. That is what this is for.
|
|
60
62
|
# WA_VERSION=2.24.10.75
|
|
61
63
|
|
|
62
64
|
# Override the static registration token. iOS only — Android has no static token
|
package/README.md
CHANGED
|
@@ -254,6 +254,8 @@ npm install -g whalibmob
|
|
|
254
254
|
- [Android Profiles](#android-profiles)
|
|
255
255
|
- [Custom Device Fields](#custom-device-fields)
|
|
256
256
|
- [Version & Token Overrides](#version--token-overrides)
|
|
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)
|
|
257
259
|
|
|
258
260
|
---
|
|
259
261
|
|
|
@@ -291,14 +293,36 @@ wa registration --request-code 919634847671 --method wa_old
|
|
|
291
293
|
```
|
|
292
294
|
|
|
293
295
|
**Registering as Android** is the same command with the platform named. Nothing
|
|
294
|
-
else to prepare — see [what it does behind that one command](#registering-as-android)
|
|
296
|
+
else to prepare — see [what it does behind that one command](#registering-as-android).
|
|
297
|
+
|
|
298
|
+
Linux, macOS, Termux:
|
|
295
299
|
|
|
296
300
|
```sh
|
|
297
301
|
WA_OS=android WA_DEVICE=samsung-s24-ultra wa registration --request-code 919634847671 --debug
|
|
298
302
|
```
|
|
299
303
|
|
|
300
|
-
|
|
301
|
-
|
|
304
|
+
Windows, Command Prompt — `set` on its own line, because `VAR=value` in front of
|
|
305
|
+
a command is Unix syntax and Windows refuses it:
|
|
306
|
+
|
|
307
|
+
```bat
|
|
308
|
+
set WA_OS=android
|
|
309
|
+
set WA_DEVICE=samsung-s24-ultra
|
|
310
|
+
wa registration --request-code 919634847671 --debug
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Windows, PowerShell:
|
|
314
|
+
|
|
315
|
+
```powershell
|
|
316
|
+
$env:WA_OS = "android"
|
|
317
|
+
$env:WA_DEVICE = "samsung-s24-ultra"
|
|
318
|
+
wa registration --request-code 919634847671 --debug
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A `.env` file in the directory you run from works the same everywhere and saves
|
|
322
|
+
repeating it — see [Device Quick Start](#device-quick-start). `--debug` prints
|
|
323
|
+
every request and reply, which is worth having the first time: its first line
|
|
324
|
+
names the platform that actually went out, so you can see whether the variables
|
|
325
|
+
arrived.
|
|
302
326
|
|
|
303
327
|
The variables only matter for the command that *creates* the session. What a
|
|
304
328
|
session registered as is written into it, so the confirmation step and every
|
|
@@ -376,6 +400,14 @@ Use a custom session directory with `--session`:
|
|
|
376
400
|
wa connect 919634847671 --session /data/my-sessions
|
|
377
401
|
```
|
|
378
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
|
+
|
|
379
411
|
### CLI Pairing Code
|
|
380
412
|
|
|
381
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:
|
|
@@ -2140,7 +2172,7 @@ Every event from the SMS primary API fires here too — `message`, `receipt`, `p
|
|
|
2140
2172
|
| `pair_device` | `{ refs }` | the QR path produced reference strings |
|
|
2141
2173
|
| `history_sync` | `{ syncTypeName, chats, contacts, pushNames, merged }` | a chunk of history arrived |
|
|
2142
2174
|
| `history_sync_error` | `{ err, notification }` | a chunk could not be fetched or decrypted |
|
|
2143
|
-
| `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. |
|
|
2144
2176
|
|
|
2145
2177
|
### Reading What the Phone Sent
|
|
2146
2178
|
|
|
@@ -4308,12 +4340,56 @@ Copy `.env.example` to `.env` in your project root and set the variables you nee
|
|
|
4308
4340
|
|
|
4309
4341
|
### Device Quick Start
|
|
4310
4342
|
|
|
4311
|
-
Emulate an Android Pixel 8 Pro
|
|
4343
|
+
Emulate an Android Pixel 8 Pro. **The syntax for setting a variable differs per
|
|
4344
|
+
shell**, and getting it wrong is the most common reason a device profile appears
|
|
4345
|
+
to be ignored:
|
|
4346
|
+
|
|
4347
|
+
**Linux, macOS, Termux** — set them for the one command:
|
|
4312
4348
|
|
|
4313
4349
|
```sh
|
|
4314
4350
|
WA_OS=android WA_DEVICE=pixel_8_pro node your-app.js
|
|
4351
|
+
WA_OS=android WA_DEVICE=pixel_8_pro wa registration --request-code 919634847671
|
|
4315
4352
|
```
|
|
4316
4353
|
|
|
4354
|
+
**Windows, Command Prompt** — `set` first, one per line. `VAR=value` in front of
|
|
4355
|
+
a command is Unix syntax and Windows answers it with
|
|
4356
|
+
`'WA_OS' is not recognized as an internal or external command`:
|
|
4357
|
+
|
|
4358
|
+
```bat
|
|
4359
|
+
set WA_OS=android
|
|
4360
|
+
set WA_DEVICE=pixel_8_pro
|
|
4361
|
+
wa registration --request-code 919634847671
|
|
4362
|
+
```
|
|
4363
|
+
|
|
4364
|
+
Keep them on separate lines. Chaining with `&&` puts the space before the `&&`
|
|
4365
|
+
inside the value.
|
|
4366
|
+
|
|
4367
|
+
**Windows, PowerShell**:
|
|
4368
|
+
|
|
4369
|
+
```powershell
|
|
4370
|
+
$env:WA_OS = "android"
|
|
4371
|
+
$env:WA_DEVICE = "pixel_8_pro"
|
|
4372
|
+
wa registration --request-code 919634847671
|
|
4373
|
+
```
|
|
4374
|
+
|
|
4375
|
+
**Anywhere, and the one worth preferring** — a `.env` file in the directory you
|
|
4376
|
+
run from, which behaves identically on every platform:
|
|
4377
|
+
|
|
4378
|
+
```dotenv
|
|
4379
|
+
WA_OS=android
|
|
4380
|
+
WA_DEVICE=pixel_8_pro
|
|
4381
|
+
```
|
|
4382
|
+
|
|
4383
|
+
```sh
|
|
4384
|
+
wa registration --request-code 919634847671
|
|
4385
|
+
```
|
|
4386
|
+
|
|
4387
|
+
The CLI reads `.env` from the **current directory**, not from where whalibmob is
|
|
4388
|
+
installed, so `cd` to the directory holding it before running. Whichever way you
|
|
4389
|
+
choose, the first line of `--debug` output tells you whether it took:
|
|
4390
|
+
`User-Agent: WhatsApp/… Android/14 Device/Google-Pixel 8 Pro`. An `iOS/…` there
|
|
4391
|
+
means the variables never arrived.
|
|
4392
|
+
|
|
4317
4393
|
Or put the variables in a `.env` file. When using the **CLI** (`wa` command) the file is loaded automatically. When using the **library directly**, load it before `require('whalibmob')`:
|
|
4318
4394
|
|
|
4319
4395
|
```js
|
|
@@ -4400,8 +4476,137 @@ These variables override individual fields on top of the selected profile:
|
|
|
4400
4476
|
|
|
4401
4477
|
| Variable | Description |
|
|
4402
4478
|
|---|---|
|
|
4403
|
-
| `WA_VERSION` | Pin the WhatsApp version (e.g. `2.24.13.80`). Skips the live store fetch. |
|
|
4404
|
-
| `WA_STATIC_TOKEN` | Override the static token used in registration token computation. |
|
|
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. |
|
|
4480
|
+
| `WA_STATIC_TOKEN` | Override the static token used in registration token computation. iOS only — Android has no static token. |
|
|
4481
|
+
|
|
4482
|
+
### When the server answers 405 on connect
|
|
4483
|
+
|
|
4484
|
+
```
|
|
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.
|
|
4489
|
+
```
|
|
4490
|
+
|
|
4491
|
+
**The session is fine and the number is still registered.** 405 is the server
|
|
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
|
|
4495
|
+
phone number and a code request.
|
|
4496
|
+
|
|
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:
|
|
4532
|
+
|
|
4533
|
+
```sh
|
|
4534
|
+
WA_VERSION=2.26.30.3 wa connect 919634847671
|
|
4535
|
+
```
|
|
4536
|
+
|
|
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.
|
|
4539
|
+
|
|
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:
|
|
4544
|
+
|
|
4545
|
+
| | how the version is found | what happens when that fails |
|
|
4546
|
+
|---|---|---|
|
|
4547
|
+
| iOS | looked up on the App Store | falls back to a version compiled into the library, which goes stale |
|
|
4548
|
+
| Android | read from the APK the registration token material came from | `wa apk-material --download` fetches the current one |
|
|
4549
|
+
|
|
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:
|
|
4554
|
+
|
|
4555
|
+
```sh
|
|
4556
|
+
wa apk-material --download
|
|
4557
|
+
```
|
|
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.
|
|
4405
4610
|
|
|
4406
4611
|
## License
|
|
4407
4612
|
|
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
|
//
|
|
@@ -575,6 +619,39 @@ function tryLoadAndroidMaterial() {
|
|
|
575
619
|
try { return loadAndroidMaterial(); } catch (_) { return null; }
|
|
576
620
|
}
|
|
577
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Which platform a registration speaks.
|
|
624
|
+
*
|
|
625
|
+
* Once a session has an account behind it, or has a code outstanding, its
|
|
626
|
+
* platform is settled: the account was created as that, the token and the
|
|
627
|
+
* User-Agent have to keep saying so, and re-reading WA_OS could only break it.
|
|
628
|
+
* A confirmation run from a shell without the variables must not flip to iOS
|
|
629
|
+
* halfway through.
|
|
630
|
+
*
|
|
631
|
+
* Before that, a session holds nothing but keys. Re-running the code request is
|
|
632
|
+
* how you start it over, and the environment is how you say what to start it
|
|
633
|
+
* over as — so the environment decides, and this stops being consulted the
|
|
634
|
+
* moment a code goes out.
|
|
635
|
+
*
|
|
636
|
+
* When the two disagree on a session that is settled, that is said out loud.
|
|
637
|
+
* Silently ignoring WA_OS is how you end up watching an Android registration go
|
|
638
|
+
* out under an iOS User-Agent and not knowing why.
|
|
639
|
+
*/
|
|
640
|
+
function deviceForRegistration(store, opts) {
|
|
641
|
+
const env = getDeviceConfig();
|
|
642
|
+
const stored = store && store.device;
|
|
643
|
+
const settled = !!(store && (store.registered || store.codePending));
|
|
644
|
+
if (!settled || !stored) return env;
|
|
645
|
+
|
|
646
|
+
if (stored.os !== env.os) {
|
|
647
|
+
const say = (opts && opts.onProgress) || (m => _whaDbg('[DBG] REG ' + m));
|
|
648
|
+
say('note: this session was started as ' + stored.os + ', so it stays ' + stored.os +
|
|
649
|
+
' — WA_OS=' + env.os + ' does not apply to a number that is already part-way ' +
|
|
650
|
+
'through registering. Delete the session file to start it over as ' + env.os + '.');
|
|
651
|
+
}
|
|
652
|
+
return stored;
|
|
653
|
+
}
|
|
654
|
+
|
|
578
655
|
/**
|
|
579
656
|
* Have the Android token material ready, fetching the APK if it is not.
|
|
580
657
|
*
|
|
@@ -1333,11 +1410,7 @@ async function requestSmsCode(store, method, opts) {
|
|
|
1333
1410
|
opts = opts || {};
|
|
1334
1411
|
// Before the version is read, since on Android the version comes out of the
|
|
1335
1412
|
// APK this fetches.
|
|
1336
|
-
|
|
1337
|
-
// store that decides — not whatever WA_OS the shell happens to carry now. A
|
|
1338
|
-
// confirmation run from a shell without the variables would otherwise sign an
|
|
1339
|
-
// Android registration with an iOS token.
|
|
1340
|
-
const _device = store.device || getDeviceConfig();
|
|
1413
|
+
const _device = deviceForRegistration(store, opts);
|
|
1341
1414
|
await ensureAndroidMaterial(opts, _device);
|
|
1342
1415
|
const waVersion = await fetchWaVersion(_device);
|
|
1343
1416
|
store.version = waVersion;
|
|
@@ -1459,11 +1532,7 @@ async function verifyCode(store, code, opts) {
|
|
|
1459
1532
|
opts = opts || {};
|
|
1460
1533
|
// Normally already there from the code request, but a confirmation can be run
|
|
1461
1534
|
// from a fresh shell that never made one.
|
|
1462
|
-
|
|
1463
|
-
// store that decides — not whatever WA_OS the shell happens to carry now. A
|
|
1464
|
-
// confirmation run from a shell without the variables would otherwise sign an
|
|
1465
|
-
// Android registration with an iOS token.
|
|
1466
|
-
const _device = store.device || getDeviceConfig();
|
|
1535
|
+
const _device = deviceForRegistration(store, opts);
|
|
1467
1536
|
await ensureAndroidMaterial(opts, _device);
|
|
1468
1537
|
const waVersion = await fetchWaVersion(_device);
|
|
1469
1538
|
store.version = waVersion;
|
|
@@ -1559,7 +1628,7 @@ module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyC
|
|
|
1559
1628
|
module.exports._http = { readHttpResponse, parseHttpResponse, decodeChunkedBody };
|
|
1560
1629
|
|
|
1561
1630
|
// Token computation, exposed for tests. Not part of the public API.
|
|
1562
|
-
module.exports._token = { computeToken, androidMaterialPath, registrationHeaders, ensureAndroidMaterial };
|
|
1631
|
+
module.exports._token = { computeToken, androidMaterialPath, registrationHeaders, ensureAndroidMaterial, deviceForRegistration };
|
|
1563
1632
|
|
|
1564
1633
|
// Challenge / two-factor internals, exposed for tests. Not part of the public API.
|
|
1565
1634
|
module.exports._verify = {
|
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,32 +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
|
-
|
|
378
|
-
version
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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),
|
|
414
|
+
// The version the session was registered with, unless WA_VERSION says
|
|
415
|
+
// otherwise. Without that override there is no way out of a 405: the
|
|
416
|
+
// version lives in the session file, the server refuses it, and the
|
|
417
|
+
// only remaining move is to register the number again — which is the
|
|
418
|
+
// one thing that should never be the answer to a client-side problem.
|
|
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.
|
|
385
441
|
phoneId: this.store.fdid.toUpperCase(),
|
|
386
442
|
releaseChannel: 0,
|
|
387
|
-
localeLanguage: _phoneMeta.lg,
|
|
388
|
-
localeCountry: _phoneMeta.lc,
|
|
389
|
-
|
|
390
|
-
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
|
|
391
450
|
}
|
|
392
451
|
});
|
|
393
452
|
|
|
@@ -463,8 +522,20 @@ class NoiseSocket extends EventEmitter {
|
|
|
463
522
|
'402': { wipe: false, text: 'temporarily banned.' },
|
|
464
523
|
'403': { wipe: true, text: "the account's primary device is gone " +
|
|
465
524
|
'— it is no longer a multi-device account.' },
|
|
466
|
-
|
|
467
|
-
|
|
525
|
+
// Not a spent session, however much it looks like one. The server
|
|
526
|
+
// declined the *client*: the version we announced is not one it
|
|
527
|
+
// accepts. The registration behind the session is untouched, and
|
|
528
|
+
// telling people to register again over this costs them a real
|
|
529
|
+
// phone number for nothing — the same version would be announced
|
|
530
|
+
// and refused identically. _handleFailure already reads it this way
|
|
531
|
+
// once a session is up; this is the same code arriving during the
|
|
532
|
+
// handshake, and it was the one path still calling it revoked.
|
|
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.' },
|
|
468
539
|
'406': { wipe: true, text: 'banned.' },
|
|
469
540
|
'409': { wipe: true, text: 'bad user agent — the client identified ' +
|
|
470
541
|
'itself in a way the server rejects.' },
|
|
@@ -475,8 +546,32 @@ class NoiseSocket extends EventEmitter {
|
|
|
475
546
|
'503': { wipe: false, text: 'service unavailable — worth retrying.' }
|
|
476
547
|
};
|
|
477
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
|
+
|
|
478
573
|
const err = new Error('WhatsApp auth failure ' + reason +
|
|
479
|
-
(
|
|
574
|
+
(detail ? ' — ' + detail : ''));
|
|
480
575
|
err.code = reason;
|
|
481
576
|
// True when the session is spent and the caller should register or
|
|
482
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
|
+
})();
|