whalibmob 5.6.3 → 5.6.8
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 +16 -4
- package/README.md +35 -0
- package/cli.js +51 -8
- package/lib/Client.js +47 -5
- package/lib/DeviceManager.js +26 -3
- package/lib/Registration.js +113 -18
- package/lib/messages/MessageSender.js +12 -3
- package/lib/noise.js +37 -2
- package/package.json +3 -2
package/.env.example
CHANGED
|
@@ -61,11 +61,23 @@
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
# ─── Proxy ───────────────────────────────────────────────────────────────────
|
|
64
|
-
# Route registration traffic through a
|
|
65
|
-
#
|
|
66
|
-
#
|
|
64
|
+
# Route registration traffic through a SOCKS proxy. Requires the `socks`
|
|
65
|
+
# package: npm install socks
|
|
66
|
+
#
|
|
67
|
+
# A bare host:port is assumed to be socks5. Add a scheme to be explicit, and
|
|
68
|
+
# credentials if the proxy needs them. Percent-encode any @ or : in a password.
|
|
69
|
+
#
|
|
70
|
+
# TOR_PROXY takes precedence over SOCKS_PROXY when both are set.
|
|
71
|
+
# The default Tor SOCKS port is 9050; the SOCKS default is 1080.
|
|
72
|
+
#
|
|
67
73
|
# TOR_PROXY=127.0.0.1:9050
|
|
68
|
-
#
|
|
74
|
+
# TOR_PROXY=socks5://127.0.0.1:9050
|
|
75
|
+
# SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
|
|
76
|
+
# SOCKS_PROXY=socks5h://proxy.example.com:1080
|
|
77
|
+
# SOCKS_PROXY=socks4://proxy.example.com:1080
|
|
78
|
+
#
|
|
79
|
+
# Only if `socks` lives somewhere require() cannot find it:
|
|
80
|
+
# WA_SOCKS_LIB=/absolute/path/to/socks/build/index.js
|
|
69
81
|
|
|
70
82
|
|
|
71
83
|
# ─── Frida attestation bridge ────────────────────────────────────────────────
|
package/README.md
CHANGED
|
@@ -150,6 +150,7 @@ npm install -g whalibmob
|
|
|
150
150
|
- [Handling Reconnects](#handling-reconnects)
|
|
151
151
|
- [Knowing Which Mode You Are In](#knowing-which-mode-you-are-in)
|
|
152
152
|
- [Two Sessions on One Number](#two-sessions-on-one-number)
|
|
153
|
+
- [Routing Registration Through a Proxy](#routing-registration-through-a-proxy)
|
|
153
154
|
- [Saving & Restoring Sessions](#saving--restoring-sessions)
|
|
154
155
|
- [Signal Store Utilities](#signal-store-utilities)
|
|
155
156
|
- [makeCacheableSignalKeyStore](#makecacheablesignalkeystore)
|
|
@@ -806,6 +807,40 @@ await client.connectWeb(PHONE) // web / companion, over WebSocket
|
|
|
806
807
|
|
|
807
808
|
Use two `WhalibmobClient` instances if you want both at once.
|
|
808
809
|
|
|
810
|
+
## Routing Registration Through a Proxy
|
|
811
|
+
|
|
812
|
+
Registration is the part of the protocol most likely to be refused from a datacenter IP. If you are seeing security blocks or an undeterminable number status, route it through a SOCKS proxy — Tor, or a residential provider.
|
|
813
|
+
|
|
814
|
+
Install the optional dependency and set one environment variable:
|
|
815
|
+
|
|
816
|
+
```sh
|
|
817
|
+
npm install socks
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
```sh
|
|
821
|
+
# Tor
|
|
822
|
+
export TOR_PROXY=socks5://127.0.0.1:9050
|
|
823
|
+
|
|
824
|
+
# a residential proxy that needs credentials
|
|
825
|
+
export SOCKS_PROXY=socks5://user:pass@proxy.example.com:1080
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
A bare `host:port` is assumed to be SOCKS5, so `TOR_PROXY=127.0.0.1:9050` works too. `socks5`, `socks5h`, `socks4` and `socks4a` are all accepted, and `TOR_PROXY` wins if both variables are set.
|
|
829
|
+
|
|
830
|
+
If the password contains an `@` or a `:`, percent-encode it — `p@ss:word` becomes `p%40ss%3Aword`. whalibmob decodes it before handing it to the proxy.
|
|
831
|
+
|
|
832
|
+
```js
|
|
833
|
+
// or from code, before you call any registration function
|
|
834
|
+
process.env.SOCKS_PROXY = 'socks5://user:pass@proxy.example.com:1080'
|
|
835
|
+
|
|
836
|
+
await requestSmsCode(phone, store)
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
> [!NOTE]
|
|
840
|
+
> This covers the registration traffic to `v.whatsapp.net`. The message socket and media transfers are not proxied — if you need those behind a proxy as well, open an issue describing the setup.
|
|
841
|
+
|
|
842
|
+
If `socks` is not installed, or a proxy is unreachable, you get a message saying so rather than a silent failure. In the rare case the package lives somewhere `require()` cannot find it, `WA_SOCKS_LIB` takes an absolute path to it.
|
|
843
|
+
|
|
809
844
|
## Saving & Restoring Sessions
|
|
810
845
|
|
|
811
846
|
Sessions are automatically persisted to disk as JSON files under the `sessionDir` you provide. The file is named `<phone>.json`. On the next `client.init()` call the session is restored and no re-registration is needed.
|
package/cli.js
CHANGED
|
@@ -576,7 +576,8 @@ function attachEvents(client) {
|
|
|
576
576
|
? 'link again with /pair'
|
|
577
577
|
: 're-register with /reg code';
|
|
578
578
|
fail('session revoked: ' + f.reason + ' — ' + how);
|
|
579
|
-
_rl &&
|
|
579
|
+
_rl && _rl.resume();
|
|
580
|
+
notConnected();
|
|
580
581
|
});
|
|
581
582
|
|
|
582
583
|
// The server declined the client, not the session. Nothing to re-register.
|
|
@@ -585,7 +586,8 @@ function attachEvents(client) {
|
|
|
585
586
|
fail('rejected by WhatsApp (' + r.reason +
|
|
586
587
|
(r.location ? ', edge ' + r.location : '') + ')');
|
|
587
588
|
out(' ' + r.message);
|
|
588
|
-
_rl &&
|
|
589
|
+
_rl && _rl.resume();
|
|
590
|
+
notConnected();
|
|
589
591
|
});
|
|
590
592
|
|
|
591
593
|
client.on('error', (e) => {
|
|
@@ -715,7 +717,7 @@ async function doConnectWeb(phone, opts) {
|
|
|
715
717
|
client.once('auth_failure', () => clearInterval(keepAlive));
|
|
716
718
|
} catch (e) {
|
|
717
719
|
fail(e.message);
|
|
718
|
-
|
|
720
|
+
notConnected();
|
|
719
721
|
}
|
|
720
722
|
}
|
|
721
723
|
|
|
@@ -735,7 +737,7 @@ async function doConnect(phone) {
|
|
|
735
737
|
client.once('auth_failure', () => {
|
|
736
738
|
fail('auth failed — session revoked');
|
|
737
739
|
out('use /reg code ' + phone + ' to re-register');
|
|
738
|
-
|
|
740
|
+
notConnected();
|
|
739
741
|
});
|
|
740
742
|
|
|
741
743
|
try {
|
|
@@ -745,11 +747,48 @@ async function doConnect(phone) {
|
|
|
745
747
|
client.once('auth_failure', () => clearInterval(keepAlive));
|
|
746
748
|
} catch (e) {
|
|
747
749
|
fail(e.message);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
+
if (/No (primary )?session for/i.test(e.message)) {
|
|
751
|
+
if (hasWebSession(phone)) {
|
|
752
|
+
out(' this number is linked as a companion — use: /pair ' + phone);
|
|
753
|
+
} else {
|
|
754
|
+
out(' register it: /reg code ' + phone + ' then /reg confirm ' + phone + ' <code>');
|
|
755
|
+
out(' or link it to an account already on a phone: /pair ' + phone);
|
|
756
|
+
}
|
|
757
|
+
} else if (e.code === '401' || /auth failure 401/i.test(e.message)) {
|
|
758
|
+
// 401 says the credentials were refused; it does not say whether the
|
|
759
|
+
// number itself is still registered. Ask, so the next step is obvious.
|
|
760
|
+
out(' checking whether the number is still registered...');
|
|
761
|
+
try {
|
|
762
|
+
const probe = await client.checkSessionAlive();
|
|
763
|
+
if (probe.alive) {
|
|
764
|
+
out(' the number IS still registered, but this device was logged out.');
|
|
765
|
+
out(' register it again: /reg code ' + phone);
|
|
766
|
+
} else if (probe.error) {
|
|
767
|
+
out(' could not reach the registration endpoint: ' + probe.error);
|
|
768
|
+
} else {
|
|
769
|
+
out(' the registration is gone server-side (' + (probe.status || 'no status') + ').');
|
|
770
|
+
out(' register it again: /reg code ' + phone);
|
|
771
|
+
}
|
|
772
|
+
} catch (probeErr) {
|
|
773
|
+
out(' status check failed: ' + probeErr.message);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
notConnected();
|
|
750
777
|
}
|
|
751
778
|
}
|
|
752
779
|
|
|
780
|
+
// Put the shell back in its disconnected state.
|
|
781
|
+
//
|
|
782
|
+
// The prompt is the only standing indication of whether there is a session, so
|
|
783
|
+
// it has to be right after a failure: a prompt reading "wa +<number>>" under an
|
|
784
|
+
// error saying there is no session for that number is the shell contradicting
|
|
785
|
+
// itself.
|
|
786
|
+
function notConnected() {
|
|
787
|
+
_client = null;
|
|
788
|
+
_phone = null;
|
|
789
|
+
if (_rl) { _rl.setPrompt('wa> '); _rl.prompt(); }
|
|
790
|
+
}
|
|
791
|
+
|
|
753
792
|
// ─── guard ────────────────────────────────────────────────────────────────────
|
|
754
793
|
|
|
755
794
|
function requireConn() {
|
|
@@ -2039,7 +2078,11 @@ async function main() {
|
|
|
2039
2078
|
const method = flags.pair || forced === 'pair' || forced === 'pairing' ? 'pairing'
|
|
2040
2079
|
: flags.sms || forced === 'sms' ? 'sms'
|
|
2041
2080
|
: await askLoginMethod(phone);
|
|
2042
|
-
|
|
2081
|
+
// Plain `wa>` until the connection actually opens. Naming the number in the
|
|
2082
|
+
// prompt before that says "connected as this number" while the line above
|
|
2083
|
+
// it says there is no session, which is the opposite of what happened —
|
|
2084
|
+
// doConnect sets the real prompt from the 'connected' event.
|
|
2085
|
+
openShell();
|
|
2043
2086
|
out('connecting to +' + phone + '...');
|
|
2044
2087
|
if (method === 'pairing') await doConnectWeb(phone);
|
|
2045
2088
|
else await doConnect(phone);
|
|
@@ -2053,7 +2096,7 @@ async function main() {
|
|
|
2053
2096
|
if (!phone) { fail('phone number required'); process.exit(1); }
|
|
2054
2097
|
const custom = (sub ? pos[0] : pos[1]) ||
|
|
2055
2098
|
(typeof flags.code === 'string' ? flags.code : undefined);
|
|
2056
|
-
openShell(
|
|
2099
|
+
openShell();
|
|
2057
2100
|
out('linking +' + phone + ' to an existing WhatsApp account...');
|
|
2058
2101
|
await doConnectWeb(phone, { customCode: custom });
|
|
2059
2102
|
return;
|
package/lib/Client.js
CHANGED
|
@@ -4,6 +4,7 @@ const { dbg: _whaDbg, configureLogger: _whaConfigLogger } = require('./logger');
|
|
|
4
4
|
|
|
5
5
|
const EventEmitter = require('events');
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
7
8
|
const crypto = require('crypto');
|
|
8
9
|
const { NoiseSocket } = require('./noise');
|
|
9
10
|
const { MessageSender, generateMessageId, makeJid, buildOrGetAdvIdentity } = require('./messages/MessageSender');
|
|
@@ -278,7 +279,19 @@ class WhalibmobClient extends EventEmitter {
|
|
|
278
279
|
const sessionFile = path.join(this._sessionDir, `${phoneNumber}.json`);
|
|
279
280
|
this._store = loadStore(sessionFile);
|
|
280
281
|
if (!this._store) {
|
|
281
|
-
|
|
282
|
+
// Say what is missing, and say it accurately. The old text pointed at
|
|
283
|
+
// 'wa registration -R', a flag this CLI has never accepted, so anyone who
|
|
284
|
+
// followed it got a second error instead of a registered number. A
|
|
285
|
+
// library has no business teaching CLI syntax either — the caller knows
|
|
286
|
+
// which interface it is; this only has to name the state.
|
|
287
|
+
const hasWeb = fs.existsSync(path.join(this._sessionDir, `${phoneNumber}.web.json`));
|
|
288
|
+
throw new Error(
|
|
289
|
+
hasWeb
|
|
290
|
+
? `No primary session for ${phoneNumber}, but it is linked as a ` +
|
|
291
|
+
'companion device — use connectWeb() rather than init().'
|
|
292
|
+
: `No session for ${phoneNumber} — register the number first, or ` +
|
|
293
|
+
'link it to an existing account with connectWeb().'
|
|
294
|
+
);
|
|
282
295
|
}
|
|
283
296
|
|
|
284
297
|
const signalFile = path.join(this._sessionDir, `${phoneNumber}.signal.json`);
|
|
@@ -766,6 +779,28 @@ class WhalibmobClient extends EventEmitter {
|
|
|
766
779
|
this._closing = false;
|
|
767
780
|
}
|
|
768
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Ask the server whether this session's registration is still alive.
|
|
784
|
+
*
|
|
785
|
+
* The read-only /exist probe, run with the credentials already on disk. It
|
|
786
|
+
* answers the question a 401 leaves open: whether the number is still
|
|
787
|
+
* registered and only this device was cut loose, or whether the registration
|
|
788
|
+
* is gone entirely. Sends nothing to the phone and requests no code, so it
|
|
789
|
+
* is safe to run on a failure.
|
|
790
|
+
*
|
|
791
|
+
* @returns {Promise<{alive: boolean, status: string|null, raw: object|null, error?: string}>}
|
|
792
|
+
*/
|
|
793
|
+
async checkSessionAlive() {
|
|
794
|
+
if (!this._store) throw new Error('No session loaded');
|
|
795
|
+
try {
|
|
796
|
+
const res = await checkIfRegistered(this._store);
|
|
797
|
+
const status = (res && (res.status || res.reason)) || null;
|
|
798
|
+
return { alive: status === 'ok', status, raw: res };
|
|
799
|
+
} catch (err) {
|
|
800
|
+
return { alive: false, status: null, raw: null, error: err.message };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
769
804
|
// ─── Reconnection ─────────────────────────────────────────────────────────
|
|
770
805
|
|
|
771
806
|
_scheduleReconnect() {
|
|
@@ -2140,10 +2175,17 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2140
2175
|
return;
|
|
2141
2176
|
}
|
|
2142
2177
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
//
|
|
2146
|
-
|
|
2178
|
+
// Not every failure kills the session. whatsmeow's IsLoggedOut() marks the
|
|
2179
|
+
// ones that do — 401, 403, 405, 406, 409 — and treats the rest as worth
|
|
2180
|
+
// another attempt. A 500 or a 503 is the server having a bad minute; giving
|
|
2181
|
+
// up on the session for that would be throwing away a working registration.
|
|
2182
|
+
const LOGGED_OUT = ['401', '403', '405', '406', '409'];
|
|
2183
|
+
const spent = LOGGED_OUT.includes(reason);
|
|
2184
|
+
|
|
2185
|
+
_whaDbg('[DBG] FAILURE reason=' + reason + ' location=' + loc +
|
|
2186
|
+
' loggedOut=' + spent);
|
|
2187
|
+
this.emit('auth_failure', { reason, location: loc, loggedOut: spent, node });
|
|
2188
|
+
this._fatal = spent;
|
|
2147
2189
|
this._reconnecting = false;
|
|
2148
2190
|
this.disconnect();
|
|
2149
2191
|
}
|
package/lib/DeviceManager.js
CHANGED
|
@@ -901,7 +901,10 @@ class DeviceManager {
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
-
|
|
904
|
+
// Same rule as the phone-addressed path: skip the device we are, not the
|
|
905
|
+
// device that happens to be numbered zero.
|
|
906
|
+
const selfDevice = this._ownDeviceIndex();
|
|
907
|
+
const deviceIds = (this._dcGet(cacheKey) || []).filter(d => d !== selfDevice);
|
|
905
908
|
const deviceJids = deviceIds.map(d => makeDeviceJid(lidUser, d, 'lid'));
|
|
906
909
|
if (deviceJids.length === 0) return [];
|
|
907
910
|
|
|
@@ -1085,10 +1088,30 @@ class DeviceManager {
|
|
|
1085
1088
|
// On the first call we query usync, on all subsequent calls we return
|
|
1086
1089
|
// from cache — zero extra network round-trips.
|
|
1087
1090
|
//
|
|
1091
|
+
// Which device index this client occupies.
|
|
1092
|
+
//
|
|
1093
|
+
// A device registered over SMS is the account's primary and is always 0. A
|
|
1094
|
+
// companion linked by pairing code occupies a numbered slot — and then
|
|
1095
|
+
// device 0 is the owner's phone, which is a recipient like any other.
|
|
1096
|
+
_ownDeviceIndex() {
|
|
1097
|
+
const store = this._client && this._client._store;
|
|
1098
|
+
return (store && store.deviceIndex) || 0;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1088
1101
|
async ensureOwnDeviceSessions(ownPhone, signalProto, allowPkmsg = true) {
|
|
1102
|
+
// Everything the account owns except the device doing the sending.
|
|
1103
|
+
//
|
|
1104
|
+
// This used to drop device 0 unconditionally, which is right only while we
|
|
1105
|
+
// are device 0. As a companion it silently excluded the owner's own phone
|
|
1106
|
+
// from every message we sent: the recipient got it, the phone did not, and
|
|
1107
|
+
// it sat there showing "waiting for this message" forever. It also put our
|
|
1108
|
+
// participant list out of step with the server's, which is what the phash
|
|
1109
|
+
// mismatch on the ack was reporting.
|
|
1110
|
+
const selfDevice = this._ownDeviceIndex();
|
|
1111
|
+
|
|
1089
1112
|
// Use cached own device list if it hasn't expired (5-min TTL matches _deviceCache)
|
|
1090
1113
|
if (this._ownDeviceJids !== null && Date.now() < this._ownDeviceExpiry) {
|
|
1091
|
-
const others = this._ownDeviceJids.filter(j => stripUser(j).device !==
|
|
1114
|
+
const others = this._ownDeviceJids.filter(j => stripUser(j).device !== selfDevice);
|
|
1092
1115
|
return this._ensureSessions(others, signalProto, allowPkmsg);
|
|
1093
1116
|
}
|
|
1094
1117
|
|
|
@@ -1097,7 +1120,7 @@ class DeviceManager {
|
|
|
1097
1120
|
this._ownDeviceJids = deviceJids;
|
|
1098
1121
|
this._ownDeviceExpiry = Date.now() + 5 * 60 * 1000;
|
|
1099
1122
|
|
|
1100
|
-
const others = deviceJids.filter(j => stripUser(j).device !==
|
|
1123
|
+
const others = deviceJids.filter(j => stripUser(j).device !== selfDevice);
|
|
1101
1124
|
return this._ensureSessions(others, signalProto, allowPkmsg);
|
|
1102
1125
|
}
|
|
1103
1126
|
|
package/lib/Registration.js
CHANGED
|
@@ -7,23 +7,109 @@ const curveJs = require('curve25519-js');
|
|
|
7
7
|
const { getDeviceConfig } = require('./DeviceConfig');
|
|
8
8
|
const attestation = require('./Attestation');
|
|
9
9
|
|
|
10
|
-
// ---------- SOCKS5 / Tor support ----------
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
10
|
+
// ---------- SOCKS4 / SOCKS5 / Tor support ----------
|
|
11
|
+
//
|
|
12
|
+
// Set SOCKS_PROXY (or TOR_PROXY) to route registration traffic through a proxy:
|
|
13
|
+
//
|
|
14
|
+
// socks5://127.0.0.1:9050 Tor
|
|
15
|
+
// socks5://user:pass@host:1080 authenticated residential proxy
|
|
16
|
+
// socks5h://host:1080 resolve the destination at the proxy
|
|
17
|
+
// socks4://host:1080 older proxies
|
|
18
|
+
//
|
|
19
|
+
// Requires the `socks` package: npm install socks
|
|
20
|
+
|
|
21
|
+
const SOCKS_SCHEMES = {
|
|
22
|
+
'socks:': 5,
|
|
23
|
+
'socks5:': 5,
|
|
24
|
+
'socks5h:': 5,
|
|
25
|
+
'socks4:': 4,
|
|
26
|
+
'socks4a:': 4
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Resolve the library the way every other dependency is resolved.
|
|
30
|
+
//
|
|
31
|
+
// This used to be a hardcoded absolute path into one particular machine's
|
|
32
|
+
// global npm directory. It resolved there and nowhere else, so proxy support
|
|
33
|
+
// was dead for everybody who installed whalibmob normally — the require threw
|
|
34
|
+
// MODULE_NOT_FOUND the moment a proxy was configured. WA_SOCKS_LIB stays as an
|
|
35
|
+
// escape hatch for unusual layouts, but it is an override now, not the only
|
|
36
|
+
// way in.
|
|
37
|
+
function loadSocks() {
|
|
38
|
+
const override = process.env.WA_SOCKS_LIB;
|
|
39
|
+
if (override) {
|
|
40
|
+
try { return require(override); }
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new Error('WA_SOCKS_LIB points at ' + override +
|
|
43
|
+
' but it could not be loaded: ' + err.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return require('socks');
|
|
48
|
+
} catch (_) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"A SOCKS proxy is configured but the 'socks' package is not installed. " +
|
|
51
|
+
'Run: npm install socks'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a proxy URL into what SocksClient expects.
|
|
58
|
+
*
|
|
59
|
+
* Accepts the schemes above, with or without credentials, and defaults the
|
|
60
|
+
* port to 1080 the way every SOCKS client does.
|
|
61
|
+
*/
|
|
62
|
+
function parseSocksProxy(proxyUrl) {
|
|
63
|
+
let raw = String(proxyUrl || '').trim();
|
|
64
|
+
if (!raw) throw new Error('No SOCKS proxy configured');
|
|
65
|
+
|
|
66
|
+
// A bare host:port is what .env.example has always shown and what most people
|
|
67
|
+
// type. Without a scheme `new URL` reads "127.0.0.1:" as the protocol and the
|
|
68
|
+
// rest as the path, so it has to be filled in before parsing.
|
|
69
|
+
if (!/^[a-z0-9+.-]+:\/\//i.test(raw)) raw = 'socks5://' + raw;
|
|
70
|
+
|
|
71
|
+
let url;
|
|
72
|
+
try { url = new URL(raw); }
|
|
73
|
+
catch (_) { throw new Error('Invalid SOCKS proxy URL: ' + proxyUrl); }
|
|
74
|
+
|
|
75
|
+
const type = SOCKS_SCHEMES[url.protocol];
|
|
76
|
+
if (!type) {
|
|
77
|
+
throw new Error('Unsupported proxy scheme "' + url.protocol + '" — use ' +
|
|
78
|
+
Object.keys(SOCKS_SCHEMES).map(s => s.replace(':', '')).join(', '));
|
|
79
|
+
}
|
|
80
|
+
if (!url.hostname) throw new Error('SOCKS proxy URL has no host: ' + proxyUrl);
|
|
81
|
+
|
|
82
|
+
const proxy = {
|
|
83
|
+
host: url.hostname,
|
|
84
|
+
port: parseInt(url.port, 10) || 1080,
|
|
85
|
+
type
|
|
86
|
+
};
|
|
87
|
+
// Credentials may be percent-encoded in a URL; a password with an @ or a :
|
|
88
|
+
// in it is common enough on paid proxies to be worth decoding.
|
|
89
|
+
if (url.username) proxy.userId = decodeURIComponent(url.username);
|
|
90
|
+
if (url.password) proxy.password = decodeURIComponent(url.password);
|
|
91
|
+
return proxy;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// TOR_PROXY wins when both are set — the precedence .env.example has always
|
|
95
|
+
// documented.
|
|
96
|
+
function socksProxyUrl() {
|
|
97
|
+
return process.env.TOR_PROXY || process.env.SOCKS_PROXY || '';
|
|
98
|
+
}
|
|
14
99
|
|
|
15
100
|
async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader) {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const dHost = 'v.whatsapp.net';
|
|
20
|
-
const dPort = 443;
|
|
101
|
+
const proxy = parseSocksProxy(proxyUrl);
|
|
102
|
+
const dHost = 'v.whatsapp.net';
|
|
103
|
+
const dPort = 443;
|
|
21
104
|
|
|
22
|
-
const { SocksClient } =
|
|
105
|
+
const { SocksClient } = loadSocks();
|
|
23
106
|
const { socket: rawSocket } = await SocksClient.createConnection({
|
|
24
|
-
proxy
|
|
107
|
+
proxy,
|
|
25
108
|
command: 'connect',
|
|
26
|
-
destination: { host: dHost, port: dPort }
|
|
109
|
+
destination: { host: dHost, port: dPort },
|
|
110
|
+
// Without this a proxy that accepts the TCP connection and then says
|
|
111
|
+
// nothing leaves the whole registration hanging with no error.
|
|
112
|
+
timeout: 20000
|
|
27
113
|
});
|
|
28
114
|
|
|
29
115
|
const tlsSocket = tls.connect({
|
|
@@ -50,16 +136,25 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader) {
|
|
|
50
136
|
`Content-Length: ${Buffer.byteLength(body)}`
|
|
51
137
|
];
|
|
52
138
|
if (authHeader) reqLines.push(`Authorization: ${authHeader}`);
|
|
53
|
-
|
|
139
|
+
// close, not keep-alive: the response is read by waiting for the server to
|
|
140
|
+
// end the stream. Asking it to keep the connection open means that never
|
|
141
|
+
// happens and the request hangs until something else times out.
|
|
142
|
+
reqLines.push(`Connection: close`, '', body);
|
|
54
143
|
const req = reqLines.join('\r\n');
|
|
55
144
|
|
|
56
145
|
tlsSocket.write(req);
|
|
57
146
|
|
|
58
147
|
const chunks = [];
|
|
59
148
|
await new Promise((res, rej) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
tlsSocket.destroy();
|
|
151
|
+
rej(new Error('SOCKS proxy timed out reading ' + path));
|
|
152
|
+
}, 30000);
|
|
153
|
+
const finish = (fn, arg) => { clearTimeout(timer); fn(arg); };
|
|
154
|
+
tlsSocket.on('data', d => chunks.push(d));
|
|
155
|
+
tlsSocket.on('end', () => finish(res));
|
|
156
|
+
tlsSocket.on('close', () => finish(res));
|
|
157
|
+
tlsSocket.on('error', e => finish(rej, e));
|
|
63
158
|
});
|
|
64
159
|
|
|
65
160
|
const raw = Buffer.concat(chunks);
|
|
@@ -510,7 +605,7 @@ function encryptPayload(plaintext) {
|
|
|
510
605
|
// ---------- HTTP ----------
|
|
511
606
|
|
|
512
607
|
function httpPost(path, body, waVersion, authHeader) {
|
|
513
|
-
const proxy =
|
|
608
|
+
const proxy = socksProxyUrl();
|
|
514
609
|
if (proxy) return httpPostViaSocks(path, body, waVersion, proxy, authHeader);
|
|
515
610
|
|
|
516
611
|
return new Promise((resolve, reject) => {
|
|
@@ -841,4 +936,4 @@ async function verifyCode(store, code) {
|
|
|
841
936
|
throw new Error(`Verification failed: ${reason || JSON.stringify(result)}`);
|
|
842
937
|
}
|
|
843
938
|
|
|
844
|
-
module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, getCountryMeta, assertRegistrationKeys };
|
|
939
|
+
module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, getCountryMeta, assertRegistrationKeys, parseSocksProxy, socksProxyUrl };
|
|
@@ -1259,8 +1259,8 @@ class MessageSender {
|
|
|
1259
1259
|
: Promise.resolve([]),
|
|
1260
1260
|
// Our own linked devices, in the address family the stanza is routed with.
|
|
1261
1261
|
// ensureOwnDeviceSessions hands back @s.whatsapp.net JIDs, which cannot go
|
|
1262
|
-
// into a stanza carrying addressing_mode='lid'; both helpers
|
|
1263
|
-
//
|
|
1262
|
+
// into a stanza carrying addressing_mode='lid'; both helpers leave out the
|
|
1263
|
+
// device we are, which is device 0 only while we are the primary.
|
|
1264
1264
|
useLid
|
|
1265
1265
|
? this._devMgr.ensureOwnLidDeviceSessions(myLidUser, ownPhone, this._signal)
|
|
1266
1266
|
: this._devMgr.ensureOwnDeviceSessions(ownPhone, this._signal)
|
|
@@ -1275,8 +1275,17 @@ class MessageSender {
|
|
|
1275
1275
|
const s = String(jid);
|
|
1276
1276
|
return s.endsWith('@hosted') || s.endsWith('@hosted.lid');
|
|
1277
1277
|
};
|
|
1278
|
+
// The sending device counts in the enumeration even though it is not a
|
|
1279
|
+
// recipient — and that is *this* device, not device 0. On a companion link
|
|
1280
|
+
// the bare JID names the owner's phone, which arrives through ownTargets
|
|
1281
|
+
// like every other device; adding it here again while leaving our own slot
|
|
1282
|
+
// out would hash a set the server never computes.
|
|
1283
|
+
const ownSelfJid = (useLid
|
|
1284
|
+
? this._client._ownDeviceLidJid()
|
|
1285
|
+
: this._client._ownDeviceJid()) || selfJid;
|
|
1286
|
+
|
|
1278
1287
|
const enumeratedDevices = [...new Set(
|
|
1279
|
-
[...memberDevices, ...lidDevices, ...ownTargets,
|
|
1288
|
+
[...memberDevices, ...lidDevices, ...ownTargets, ownSelfJid]
|
|
1280
1289
|
)].filter(jid => !isHostedJid(jid));
|
|
1281
1290
|
|
|
1282
1291
|
// The participant hash the server checks the fan-out against. whatsmeow puts
|
package/lib/noise.js
CHANGED
|
@@ -444,9 +444,44 @@ class NoiseSocket extends EventEmitter {
|
|
|
444
444
|
|
|
445
445
|
if (tag === 'failure') {
|
|
446
446
|
const attrs = (node && node.attrs) || {};
|
|
447
|
-
const reason = attrs.reason || attrs.location || '401';
|
|
448
|
-
|
|
447
|
+
const reason = String(attrs.reason || attrs.location || '401');
|
|
448
|
+
// Say what the code means. "auth failure: 401" sends people looking
|
|
449
|
+
// for a bug in the client; the credentials were rejected by the
|
|
450
|
+
// server, which is a different thing and has different remedies.
|
|
451
|
+
//
|
|
452
|
+
// The names and numbers are whatsmeow's ConnectFailureReason, which
|
|
453
|
+
// is the only place they are written down — WhatsApp publishes none
|
|
454
|
+
// of this. Codes marked "wipe" are the ones its IsLoggedOut() covers:
|
|
455
|
+
// the session is finished and retrying with it is pointless.
|
|
456
|
+
const FAILURE = {
|
|
457
|
+
'400': { wipe: false, text: 'generic failure.' },
|
|
458
|
+
'401': { wipe: true, text: 'logged out. The session was unpaired ' +
|
|
459
|
+
'server-side — nothing in the session file can revive it. ' +
|
|
460
|
+
'For a linked device this means it was removed under ' +
|
|
461
|
+
'Linked Devices; for a registered number it means the ' +
|
|
462
|
+
'registration was taken over, usually by verifying the ' +
|
|
463
|
+
'same number on the official app.' },
|
|
464
|
+
'402': { wipe: false, text: 'temporarily banned.' },
|
|
465
|
+
'403': { wipe: true, text: "the account's primary device is gone " +
|
|
466
|
+
'— it is no longer a multi-device account.' },
|
|
467
|
+
'405': { wipe: true, text: 'client outdated — the announced ' +
|
|
468
|
+
'version is not one the server accepts.' },
|
|
469
|
+
'406': { wipe: true, text: 'banned.' },
|
|
470
|
+
'409': { wipe: true, text: 'bad user agent — the client identified ' +
|
|
471
|
+
'itself in a way the server rejects.' },
|
|
472
|
+
'413': { wipe: false, text: 'auth token expired.' },
|
|
473
|
+
'414': { wipe: false, text: 'auth token invalid.' },
|
|
474
|
+
'415': { wipe: false, text: 'not found.' },
|
|
475
|
+
'500': { wipe: false, text: 'server error — worth retrying.' },
|
|
476
|
+
'503': { wipe: false, text: 'service unavailable — worth retrying.' }
|
|
477
|
+
};
|
|
478
|
+
const info = FAILURE[reason];
|
|
479
|
+
const err = new Error('WhatsApp auth failure ' + reason +
|
|
480
|
+
(info ? ' — ' + info.text : ''));
|
|
449
481
|
err.code = reason;
|
|
482
|
+
// True when the session is spent and the caller should register or
|
|
483
|
+
// link again rather than retry.
|
|
484
|
+
err.loggedOut = !!(info && info.wipe);
|
|
450
485
|
if (this._connectReject) {
|
|
451
486
|
this._connectReject(err);
|
|
452
487
|
this._connectResolve = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whalibmob",
|
|
3
|
-
"version": "5.6.
|
|
3
|
+
"version": "5.6.8",
|
|
4
4
|
"description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
|
|
5
5
|
"author": "Kunboruto50",
|
|
6
6
|
"main": "index.js",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"ws": "^8.21.1"
|
|
52
52
|
},
|
|
53
53
|
"optionalDependencies": {
|
|
54
|
-
"jimp": "^1.6.0"
|
|
54
|
+
"jimp": "^1.6.0",
|
|
55
|
+
"socks": "^2.8.9"
|
|
55
56
|
}
|
|
56
57
|
}
|