@zapo-js/fake-server 0.3.0 → 1.1.0
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 +96 -8
- package/dist/api/AppStateSyncManager.d.ts +0 -1
- package/dist/api/AppStateSyncManager.js +0 -1
- package/dist/api/FakePairingDriver.d.ts +0 -1
- package/dist/api/FakePairingDriver.js +0 -1
- package/dist/api/FakePeer.d.ts +77 -4
- package/dist/api/FakePeer.js +208 -2
- package/dist/api/FakeServerSession.d.ts +67 -0
- package/dist/api/FakeServerSession.js +293 -0
- package/dist/api/FakeWaServer.d.ts +77 -31
- package/dist/api/FakeWaServer.js +140 -259
- package/dist/api/PreKeyDispenser.d.ts +0 -1
- package/dist/api/PreKeyDispenser.js +0 -1
- package/dist/api/Scenario.d.ts +0 -1
- package/dist/api/Scenario.js +0 -1
- package/dist/api/ServerRegistries.d.ts +0 -1
- package/dist/api/ServerRegistries.js +0 -1
- package/dist/api/iq-handlers.d.ts +1 -1
- package/dist/api/iq-handlers.js +23 -1
- package/dist/{cli.d.ts → bin.d.ts} +0 -1
- package/dist/bin.js +296 -0
- package/dist/esm/api/AppStateSyncManager.js +146 -0
- package/dist/esm/api/FakePairingDriver.js +31 -0
- package/dist/esm/api/FakePeer.js +610 -0
- package/dist/esm/api/FakeServerSession.js +287 -0
- package/dist/esm/api/FakeWaServer.js +528 -0
- package/dist/esm/api/PreKeyDispenser.js +93 -0
- package/dist/esm/api/Scenario.js +32 -0
- package/dist/esm/api/ServerRegistries.js +261 -0
- package/dist/esm/api/iq-handlers.js +515 -0
- package/dist/{cli.js → esm/bin.js} +70 -25
- package/dist/esm/index.js +26 -0
- package/dist/esm/infra/WaFakeConnection.js +49 -0
- package/dist/esm/infra/WaFakeConnectionPipeline.js +235 -0
- package/dist/esm/infra/WaFakeFrameSocket.js +117 -0
- package/dist/esm/infra/WaFakeMediaHttpsServer.js +126 -0
- package/dist/esm/infra/WaFakeNoiseHandshake.js +67 -0
- package/dist/esm/infra/WaFakeTransport.js +31 -0
- package/dist/esm/infra/WaFakeWsServer.js +96 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/protocol/auth/cert-chain.js +50 -0
- package/dist/esm/protocol/auth/client-payload-validate.js +55 -0
- package/dist/esm/protocol/auth/fake-primary-device.js +47 -0
- package/dist/esm/protocol/auth/pair-device.js +87 -0
- package/dist/esm/protocol/auth/success-node.js +26 -0
- package/dist/esm/protocol/iq/abprops.js +30 -0
- package/dist/esm/protocol/iq/appstate-sync.js +146 -0
- package/dist/esm/protocol/iq/business.js +59 -0
- package/dist/esm/protocol/iq/dirty-bits.js +19 -0
- package/dist/esm/protocol/iq/group-ops.js +154 -0
- package/dist/esm/protocol/iq/newsletter.js +14 -0
- package/dist/esm/protocol/iq/prekey-fetch.js +88 -0
- package/dist/esm/protocol/iq/privacy-token.js +26 -0
- package/dist/esm/protocol/iq/privacy.js +123 -0
- package/dist/esm/protocol/iq/profile.js +72 -0
- package/dist/esm/protocol/iq/router.js +117 -0
- package/dist/esm/protocol/iq/usync.js +64 -0
- package/dist/esm/protocol/push/app-state-key-share.js +26 -0
- package/dist/esm/protocol/push/call-failure.js +27 -0
- package/dist/esm/protocol/push/chatstate.js +23 -0
- package/dist/esm/protocol/push/error-stanza.js +13 -0
- package/dist/esm/protocol/push/history-sync.js +111 -0
- package/dist/esm/protocol/push/message.js +38 -0
- package/dist/esm/protocol/push/notification.js +29 -0
- package/dist/esm/protocol/push/presence.js +14 -0
- package/dist/esm/protocol/push/receipt.js +21 -0
- package/dist/esm/protocol/signal/fake-app-state-crypto.js +196 -0
- package/dist/esm/protocol/signal/fake-peer-double-ratchet.js +398 -0
- package/dist/esm/protocol/signal/fake-peer-group-recv-session.js +120 -0
- package/dist/esm/protocol/signal/fake-peer-identity.js +8 -0
- package/dist/esm/protocol/signal/fake-peer-key-bundle.js +25 -0
- package/dist/esm/protocol/signal/fake-sender-key.js +83 -0
- package/dist/esm/protocol/signal/prekey-upload.js +107 -0
- package/dist/esm/protocol/stream/stream-error.js +56 -0
- package/dist/esm/state/fake-app-state-collection.js +134 -0
- package/dist/esm/state/fake-media-store.js +53 -0
- package/dist/esm/transport/codec.js +8 -0
- package/dist/esm/transport/crypto.js +8 -0
- package/dist/esm/transport/protos.js +7 -0
- package/dist/esm/transport/util.js +8 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -2
- package/dist/infra/WaFakeConnection.d.ts +0 -1
- package/dist/infra/WaFakeConnection.js +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts +10 -3
- package/dist/infra/WaFakeConnectionPipeline.js +47 -32
- package/dist/infra/WaFakeFrameSocket.d.ts +0 -1
- package/dist/infra/WaFakeFrameSocket.js +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.d.ts +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.js +0 -1
- package/dist/infra/WaFakeNoiseHandshake.d.ts +8 -10
- package/dist/infra/WaFakeNoiseHandshake.js +19 -24
- package/dist/infra/WaFakeTransport.d.ts +2 -4
- package/dist/infra/WaFakeTransport.js +2 -1
- package/dist/infra/WaFakeWsServer.d.ts +0 -1
- package/dist/infra/WaFakeWsServer.js +0 -1
- package/dist/protocol/auth/cert-chain.d.ts +4 -1
- package/dist/protocol/auth/cert-chain.js +13 -3
- package/dist/protocol/auth/client-payload-validate.d.ts +0 -1
- package/dist/protocol/auth/client-payload-validate.js +0 -1
- package/dist/protocol/auth/fake-primary-device.d.ts +0 -1
- package/dist/protocol/auth/fake-primary-device.js +1 -3
- package/dist/protocol/auth/pair-device.d.ts +0 -1
- package/dist/protocol/auth/pair-device.js +4 -16
- package/dist/protocol/auth/success-node.d.ts +0 -1
- package/dist/protocol/auth/success-node.js +0 -1
- package/dist/protocol/iq/abprops.d.ts +0 -1
- package/dist/protocol/iq/abprops.js +0 -1
- package/dist/protocol/iq/appstate-sync.d.ts +0 -1
- package/dist/protocol/iq/appstate-sync.js +0 -1
- package/dist/protocol/iq/business.d.ts +0 -1
- package/dist/protocol/iq/business.js +0 -1
- package/dist/protocol/iq/dirty-bits.d.ts +0 -1
- package/dist/protocol/iq/dirty-bits.js +0 -1
- package/dist/protocol/iq/group-ops.d.ts +0 -1
- package/dist/protocol/iq/group-ops.js +0 -1
- package/dist/protocol/iq/newsletter.d.ts +0 -1
- package/dist/protocol/iq/newsletter.js +0 -1
- package/dist/protocol/iq/prekey-fetch.d.ts +0 -1
- package/dist/protocol/iq/prekey-fetch.js +0 -1
- package/dist/protocol/iq/privacy-token.d.ts +0 -1
- package/dist/protocol/iq/privacy-token.js +0 -1
- package/dist/protocol/iq/privacy.d.ts +0 -1
- package/dist/protocol/iq/privacy.js +15 -16
- package/dist/protocol/iq/profile.d.ts +0 -1
- package/dist/protocol/iq/profile.js +0 -1
- package/dist/protocol/iq/router.d.ts +7 -2
- package/dist/protocol/iq/router.js +8 -3
- package/dist/protocol/iq/usync.d.ts +0 -1
- package/dist/protocol/iq/usync.js +0 -1
- package/dist/protocol/push/app-state-key-share.d.ts +0 -1
- package/dist/protocol/push/app-state-key-share.js +0 -1
- package/dist/protocol/push/call-failure.d.ts +0 -1
- package/dist/protocol/push/call-failure.js +0 -1
- package/dist/protocol/push/chatstate.d.ts +0 -1
- package/dist/protocol/push/chatstate.js +0 -1
- package/dist/protocol/push/error-stanza.d.ts +0 -1
- package/dist/protocol/push/error-stanza.js +0 -1
- package/dist/protocol/push/history-sync.d.ts +0 -1
- package/dist/protocol/push/history-sync.js +0 -1
- package/dist/protocol/push/message.d.ts +0 -1
- package/dist/protocol/push/message.js +0 -1
- package/dist/protocol/push/notification.d.ts +0 -1
- package/dist/protocol/push/notification.js +0 -1
- package/dist/protocol/push/presence.d.ts +0 -1
- package/dist/protocol/push/presence.js +0 -1
- package/dist/protocol/push/receipt.d.ts +0 -1
- package/dist/protocol/push/receipt.js +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.d.ts +7 -9
- package/dist/protocol/signal/fake-app-state-crypto.js +31 -33
- package/dist/protocol/signal/fake-peer-double-ratchet.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.js +23 -28
- package/dist/protocol/signal/fake-peer-group-recv-session.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.js +6 -10
- package/dist/protocol/signal/fake-peer-identity.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-identity.js +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.d.ts +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.js +0 -1
- package/dist/protocol/signal/fake-sender-key.d.ts +0 -1
- package/dist/protocol/signal/fake-sender-key.js +5 -9
- package/dist/protocol/signal/prekey-upload.d.ts +0 -1
- package/dist/protocol/signal/prekey-upload.js +0 -1
- package/dist/protocol/stream/stream-error.d.ts +0 -1
- package/dist/protocol/stream/stream-error.js +0 -1
- package/dist/state/fake-app-state-collection.d.ts +0 -1
- package/dist/state/fake-app-state-collection.js +0 -1
- package/dist/state/fake-media-store.d.ts +0 -1
- package/dist/state/fake-media-store.js +2 -2
- package/dist/transport/codec.d.ts +2 -3
- package/dist/transport/codec.js +2 -3
- package/dist/transport/crypto.d.ts +5 -7
- package/dist/transport/crypto.js +6 -11
- package/dist/transport/protos.d.ts +2 -4
- package/dist/transport/protos.js +2 -4
- package/dist/transport/util.d.ts +8 -0
- package/dist/transport/util.js +16 -0
- package/package.json +26 -6
- package/dist/api/AppStateSyncManager.d.ts.map +0 -1
- package/dist/api/AppStateSyncManager.js.map +0 -1
- package/dist/api/FakePairingDriver.d.ts.map +0 -1
- package/dist/api/FakePairingDriver.js.map +0 -1
- package/dist/api/FakePeer.d.ts.map +0 -1
- package/dist/api/FakePeer.js.map +0 -1
- package/dist/api/FakeWaServer.d.ts.map +0 -1
- package/dist/api/FakeWaServer.js.map +0 -1
- package/dist/api/PreKeyDispenser.d.ts.map +0 -1
- package/dist/api/PreKeyDispenser.js.map +0 -1
- package/dist/api/Scenario.d.ts.map +0 -1
- package/dist/api/Scenario.js.map +0 -1
- package/dist/api/ServerRegistries.d.ts.map +0 -1
- package/dist/api/ServerRegistries.js.map +0 -1
- package/dist/api/iq-handlers.d.ts.map +0 -1
- package/dist/api/iq-handlers.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infra/WaFakeConnection.d.ts.map +0 -1
- package/dist/infra/WaFakeConnection.js.map +0 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts.map +0 -1
- package/dist/infra/WaFakeConnectionPipeline.js.map +0 -1
- package/dist/infra/WaFakeFrameSocket.d.ts.map +0 -1
- package/dist/infra/WaFakeFrameSocket.js.map +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.d.ts.map +0 -1
- package/dist/infra/WaFakeMediaHttpsServer.js.map +0 -1
- package/dist/infra/WaFakeNoiseHandshake.d.ts.map +0 -1
- package/dist/infra/WaFakeNoiseHandshake.js.map +0 -1
- package/dist/infra/WaFakeTransport.d.ts.map +0 -1
- package/dist/infra/WaFakeTransport.js.map +0 -1
- package/dist/infra/WaFakeWsServer.d.ts.map +0 -1
- package/dist/infra/WaFakeWsServer.js.map +0 -1
- package/dist/protocol/auth/cert-chain.d.ts.map +0 -1
- package/dist/protocol/auth/cert-chain.js.map +0 -1
- package/dist/protocol/auth/client-payload-validate.d.ts.map +0 -1
- package/dist/protocol/auth/client-payload-validate.js.map +0 -1
- package/dist/protocol/auth/fake-primary-device.d.ts.map +0 -1
- package/dist/protocol/auth/fake-primary-device.js.map +0 -1
- package/dist/protocol/auth/pair-device.d.ts.map +0 -1
- package/dist/protocol/auth/pair-device.js.map +0 -1
- package/dist/protocol/auth/success-node.d.ts.map +0 -1
- package/dist/protocol/auth/success-node.js.map +0 -1
- package/dist/protocol/iq/abprops.d.ts.map +0 -1
- package/dist/protocol/iq/abprops.js.map +0 -1
- package/dist/protocol/iq/appstate-sync.d.ts.map +0 -1
- package/dist/protocol/iq/appstate-sync.js.map +0 -1
- package/dist/protocol/iq/business.d.ts.map +0 -1
- package/dist/protocol/iq/business.js.map +0 -1
- package/dist/protocol/iq/dirty-bits.d.ts.map +0 -1
- package/dist/protocol/iq/dirty-bits.js.map +0 -1
- package/dist/protocol/iq/group-ops.d.ts.map +0 -1
- package/dist/protocol/iq/group-ops.js.map +0 -1
- package/dist/protocol/iq/newsletter.d.ts.map +0 -1
- package/dist/protocol/iq/newsletter.js.map +0 -1
- package/dist/protocol/iq/prekey-fetch.d.ts.map +0 -1
- package/dist/protocol/iq/prekey-fetch.js.map +0 -1
- package/dist/protocol/iq/privacy-token.d.ts.map +0 -1
- package/dist/protocol/iq/privacy-token.js.map +0 -1
- package/dist/protocol/iq/privacy.d.ts.map +0 -1
- package/dist/protocol/iq/privacy.js.map +0 -1
- package/dist/protocol/iq/profile.d.ts.map +0 -1
- package/dist/protocol/iq/profile.js.map +0 -1
- package/dist/protocol/iq/router.d.ts.map +0 -1
- package/dist/protocol/iq/router.js.map +0 -1
- package/dist/protocol/iq/usync.d.ts.map +0 -1
- package/dist/protocol/iq/usync.js.map +0 -1
- package/dist/protocol/push/app-state-key-share.d.ts.map +0 -1
- package/dist/protocol/push/app-state-key-share.js.map +0 -1
- package/dist/protocol/push/call-failure.d.ts.map +0 -1
- package/dist/protocol/push/call-failure.js.map +0 -1
- package/dist/protocol/push/chatstate.d.ts.map +0 -1
- package/dist/protocol/push/chatstate.js.map +0 -1
- package/dist/protocol/push/error-stanza.d.ts.map +0 -1
- package/dist/protocol/push/error-stanza.js.map +0 -1
- package/dist/protocol/push/history-sync.d.ts.map +0 -1
- package/dist/protocol/push/history-sync.js.map +0 -1
- package/dist/protocol/push/message.d.ts.map +0 -1
- package/dist/protocol/push/message.js.map +0 -1
- package/dist/protocol/push/notification.d.ts.map +0 -1
- package/dist/protocol/push/notification.js.map +0 -1
- package/dist/protocol/push/presence.d.ts.map +0 -1
- package/dist/protocol/push/presence.js.map +0 -1
- package/dist/protocol/push/receipt.d.ts.map +0 -1
- package/dist/protocol/push/receipt.js.map +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.d.ts.map +0 -1
- package/dist/protocol/signal/fake-app-state-crypto.js.map +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-double-ratchet.js.map +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-group-recv-session.js.map +0 -1
- package/dist/protocol/signal/fake-peer-identity.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-identity.js.map +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-key-bundle.js.map +0 -1
- package/dist/protocol/signal/fake-peer-recv-session.d.ts +0 -60
- package/dist/protocol/signal/fake-peer-recv-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-recv-session.js +0 -284
- package/dist/protocol/signal/fake-peer-recv-session.js.map +0 -1
- package/dist/protocol/signal/fake-peer-session.d.ts +0 -87
- package/dist/protocol/signal/fake-peer-session.d.ts.map +0 -1
- package/dist/protocol/signal/fake-peer-session.js +0 -207
- package/dist/protocol/signal/fake-peer-session.js.map +0 -1
- package/dist/protocol/signal/fake-sender-key.d.ts.map +0 -1
- package/dist/protocol/signal/fake-sender-key.js.map +0 -1
- package/dist/protocol/signal/prekey-upload.d.ts.map +0 -1
- package/dist/protocol/signal/prekey-upload.js.map +0 -1
- package/dist/protocol/stream/stream-error.d.ts.map +0 -1
- package/dist/protocol/stream/stream-error.js.map +0 -1
- package/dist/state/fake-app-state-collection.d.ts.map +0 -1
- package/dist/state/fake-app-state-collection.js.map +0 -1
- package/dist/state/fake-media-store.d.ts.map +0 -1
- package/dist/state/fake-media-store.js.map +0 -1
- package/dist/transport/codec.d.ts.map +0 -1
- package/dist/transport/codec.js.map +0 -1
- package/dist/transport/crypto.d.ts.map +0 -1
- package/dist/transport/crypto.js.map +0 -1
- package/dist/transport/index.d.ts +0 -4
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/index.js +0 -20
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/protos.d.ts.map +0 -1
- package/dist/transport/protos.js.map +0 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
2
|
/** Standalone CLI for `@zapo-js/fake-server`. */
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { FakeWaServer } from './api/FakeWaServer.js';
|
|
5
|
+
import { parsePairingQrString } from './protocol/auth/pair-device.js';
|
|
6
|
+
import { bytesToHex, toError } from './transport/util.js';
|
|
6
7
|
function parseArgs(argv) {
|
|
7
8
|
let host = '127.0.0.1';
|
|
8
9
|
let port = 0;
|
|
9
10
|
let path = '/ws/chat';
|
|
10
11
|
const peerJids = [];
|
|
11
12
|
const groupSpecs = [];
|
|
13
|
+
let pairJid = null;
|
|
12
14
|
let log = false;
|
|
13
15
|
let quiet = false;
|
|
14
16
|
let json = false;
|
|
@@ -46,6 +48,9 @@ function parseArgs(argv) {
|
|
|
46
48
|
case '--group':
|
|
47
49
|
groupSpecs.push(next());
|
|
48
50
|
break;
|
|
51
|
+
case '--pair':
|
|
52
|
+
pairJid = next();
|
|
53
|
+
break;
|
|
49
54
|
case '--log':
|
|
50
55
|
log = true;
|
|
51
56
|
break;
|
|
@@ -59,10 +64,10 @@ function parseArgs(argv) {
|
|
|
59
64
|
throw new Error(`unknown flag: ${arg}`);
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
|
-
return { host, port, path, peerJids, groupSpecs, log, quiet, json, help };
|
|
67
|
+
return { host, port, path, peerJids, groupSpecs, pairJid, log, quiet, json, help };
|
|
63
68
|
}
|
|
64
69
|
function printHelp() {
|
|
65
|
-
process.stdout.write(`@zapo-js/fake-server
|
|
70
|
+
process.stdout.write(`@zapo-js/fake-server – standalone fake WhatsApp Web server
|
|
66
71
|
|
|
67
72
|
USAGE
|
|
68
73
|
npx @zapo-js/fake-server [flags]
|
|
@@ -75,6 +80,12 @@ FLAGS
|
|
|
75
80
|
--group <spec> Pre-create a fake group; spec format
|
|
76
81
|
<group-jid>=<peer-jid>,<peer-jid>,...
|
|
77
82
|
All peers must already be passed via --peer.
|
|
83
|
+
--pair <jid> Drive QR pairing for unregistered clients: the server
|
|
84
|
+
sends pair-device refs, then prompts on stdin for the
|
|
85
|
+
QR payload the client displays
|
|
86
|
+
(ref,noisePubB64,identityPubB64,advSecretB64,platform)
|
|
87
|
+
and answers pair-success assigning <jid> as the device
|
|
88
|
+
jid. Without this flag the CLI is login-only.
|
|
78
89
|
--log Print every captured inbound stanza
|
|
79
90
|
--quiet Suppress the startup info banner
|
|
80
91
|
--json Print connection info as JSON
|
|
@@ -105,7 +116,7 @@ async function main() {
|
|
|
105
116
|
args = parseArgs(process.argv.slice(2));
|
|
106
117
|
}
|
|
107
118
|
catch (error) {
|
|
108
|
-
process.stderr.write(`error: ${error
|
|
119
|
+
process.stderr.write(`error: ${toError(error).message}\n`);
|
|
109
120
|
process.stderr.write('run with --help to see usage.\n');
|
|
110
121
|
process.exit(2);
|
|
111
122
|
}
|
|
@@ -122,16 +133,44 @@ async function main() {
|
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
}
|
|
125
|
-
const server = await
|
|
136
|
+
const server = await FakeWaServer.start({
|
|
126
137
|
host: args.host,
|
|
127
138
|
port: args.port,
|
|
128
139
|
path: args.path
|
|
129
140
|
});
|
|
141
|
+
if (args.pairJid !== null) {
|
|
142
|
+
const pairJid = args.pairJid;
|
|
143
|
+
let pairingStarted = false;
|
|
144
|
+
server.onAuthenticatedPipeline(async (pipeline) => {
|
|
145
|
+
if (pairingStarted)
|
|
146
|
+
return;
|
|
147
|
+
if (pipeline.clientPayload?.kind !== 'registration')
|
|
148
|
+
return;
|
|
149
|
+
pairingStarted = true;
|
|
150
|
+
try {
|
|
151
|
+
await server.runPairing(pipeline, { deviceJid: pairJid }, async () => {
|
|
152
|
+
const qr = await readStdinLine('[fake-server] paste the QR payload displayed by the client and press Enter:\n');
|
|
153
|
+
const parsed = parsePairingQrString(qr.trim());
|
|
154
|
+
return {
|
|
155
|
+
advSecretKey: parsed.advSecretKey,
|
|
156
|
+
identityPublicKey: parsed.identityPublicKey
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
process.stdout.write(`[fake-server] pair-success sent for ${pairJid}; the client will now reconnect and log in\n`);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
pairingStarted = false;
|
|
163
|
+
process.stderr.write(`pairing failed: ${toError(error).message}\n`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
130
167
|
let setupComplete = false;
|
|
131
168
|
const setupPromise = new Promise((resolve, reject) => {
|
|
132
169
|
server.onAuthenticatedPipeline(async (pipeline) => {
|
|
133
170
|
if (setupComplete)
|
|
134
171
|
return;
|
|
172
|
+
if (pipeline.clientPayload?.kind !== 'login')
|
|
173
|
+
return;
|
|
135
174
|
setupComplete = true;
|
|
136
175
|
try {
|
|
137
176
|
const peers = new Map();
|
|
@@ -157,7 +196,7 @@ async function main() {
|
|
|
157
196
|
resolve();
|
|
158
197
|
}
|
|
159
198
|
catch (error) {
|
|
160
|
-
reject(
|
|
199
|
+
reject(toError(error));
|
|
161
200
|
}
|
|
162
201
|
});
|
|
163
202
|
});
|
|
@@ -166,12 +205,11 @@ async function main() {
|
|
|
166
205
|
process.exitCode = 1;
|
|
167
206
|
});
|
|
168
207
|
if (args.log) {
|
|
169
|
-
server
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
});
|
|
208
|
+
// onCapturedStanza taps the server's own capture stream; overriding
|
|
209
|
+
// pipeline.setEvents here would replace the server's handlers and
|
|
210
|
+
// silently break --peer setup and stanza capture.
|
|
211
|
+
server.onCapturedStanza((node) => {
|
|
212
|
+
process.stdout.write(`[wire] ${node.tag}${node.attrs.id ? ` id=${node.attrs.id}` : ''}${node.attrs.type ? ` type=${node.attrs.type}` : ''}\n`);
|
|
175
213
|
});
|
|
176
214
|
}
|
|
177
215
|
if (!args.quiet) {
|
|
@@ -187,7 +225,8 @@ async function main() {
|
|
|
187
225
|
publicKeyHex: bytesToHex(noiseRoot.publicKey)
|
|
188
226
|
},
|
|
189
227
|
peers: args.peerJids,
|
|
190
|
-
groups: parsedGroups
|
|
228
|
+
groups: parsedGroups,
|
|
229
|
+
pair: args.pairJid
|
|
191
230
|
}, null, 2) + '\n');
|
|
192
231
|
}
|
|
193
232
|
else {
|
|
@@ -203,6 +242,7 @@ async function main() {
|
|
|
203
242
|
`│ noise root ca serial=${noiseRoot.serial} pub=${bytesToHex(noiseRoot.publicKey).slice(0, 32)}…`,
|
|
204
243
|
`│ peers ${args.peerJids.length === 0 ? '(none)' : args.peerJids.join(', ')}`,
|
|
205
244
|
`│ groups ${parsedGroups.length === 0 ? '(none)' : parsedGroups.map((g) => g.groupJid).join(', ')}`,
|
|
245
|
+
`│ pair mode ${args.pairJid ?? '(off – login only)'}`,
|
|
206
246
|
'├─────────────────────────────────────────────────────────────',
|
|
207
247
|
'│ Wire your WaClient with:',
|
|
208
248
|
'│ chatSocketUrls: [server.url]',
|
|
@@ -226,7 +266,7 @@ async function main() {
|
|
|
226
266
|
process.exit(0);
|
|
227
267
|
}
|
|
228
268
|
catch (error) {
|
|
229
|
-
process.stderr.write(`error during shutdown: ${error
|
|
269
|
+
process.stderr.write(`error during shutdown: ${toError(error).message}\n`);
|
|
230
270
|
process.exit(1);
|
|
231
271
|
}
|
|
232
272
|
};
|
|
@@ -234,16 +274,21 @@ async function main() {
|
|
|
234
274
|
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
235
275
|
await new Promise(() => undefined);
|
|
236
276
|
}
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
277
|
+
function readStdinLine(prompt) {
|
|
278
|
+
process.stdout.write(prompt);
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const rl = createInterface({ input: process.stdin });
|
|
281
|
+
// resolve before close(): rl.close() emits 'close' synchronously,
|
|
282
|
+
// which would otherwise reject the still-pending promise first.
|
|
283
|
+
rl.once('line', (line) => {
|
|
284
|
+
resolve(line);
|
|
285
|
+
rl.close();
|
|
286
|
+
});
|
|
287
|
+
rl.once('close', () => reject(new Error('stdin closed before a QR payload was provided')));
|
|
288
|
+
});
|
|
244
289
|
}
|
|
245
290
|
main().catch((error) => {
|
|
246
|
-
|
|
291
|
+
const normalized = toError(error);
|
|
292
|
+
process.stderr.write(`fatal: ${normalized.stack ?? normalized.message}\n`);
|
|
247
293
|
process.exit(1);
|
|
248
294
|
});
|
|
249
|
-
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { FakePairingDriver } from './api/FakePairingDriver.js';
|
|
2
|
+
export { FakePeer } from './api/FakePeer.js';
|
|
3
|
+
export { FakeServerSession, FakeWaServer } from './api/FakeWaServer.js';
|
|
4
|
+
export { IqExpectation, Scenario } from './api/Scenario.js';
|
|
5
|
+
export { WaFakeConnection } from './infra/WaFakeConnection.js';
|
|
6
|
+
export { buildAppStateSyncFullResult, buildAppStateSyncResult, buildServerSyncNotification, parseAppStateSyncRequest } from './protocol/iq/appstate-sync.js';
|
|
7
|
+
export { buildIqError, buildIqResult } from './protocol/iq/router.js';
|
|
8
|
+
export { buildCall, buildFailure } from './protocol/push/call-failure.js';
|
|
9
|
+
export { buildChatstate } from './protocol/push/chatstate.js';
|
|
10
|
+
export { buildIncomingErrorStanza } from './protocol/push/error-stanza.js';
|
|
11
|
+
export { buildAppStateSyncKeyShareMessage } from './protocol/push/app-state-key-share.js';
|
|
12
|
+
export { buildHistorySyncExternalMessage, buildHistorySyncMessage, encodeHistorySyncPlaintext } from './protocol/push/history-sync.js';
|
|
13
|
+
export { APP_STATE_EMPTY_LT_HASH, FakeAppStateCrypto } from './protocol/signal/fake-app-state-crypto.js';
|
|
14
|
+
export { FakeAppStateCollection } from './state/fake-app-state-collection.js';
|
|
15
|
+
export { FakeMediaStore } from './state/fake-media-store.js';
|
|
16
|
+
export { buildMessage } from './protocol/push/message.js';
|
|
17
|
+
export { buildGroupNotification, buildNotification } from './protocol/push/notification.js';
|
|
18
|
+
export { buildIncomingPresence } from './protocol/push/presence.js';
|
|
19
|
+
export { buildReceipt } from './protocol/push/receipt.js';
|
|
20
|
+
export { generateFakePeerIdentity } from './protocol/signal/fake-peer-identity.js';
|
|
21
|
+
export { FakePeerDoubleRatchet } from './protocol/signal/fake-peer-double-ratchet.js';
|
|
22
|
+
export { FakeSenderKey } from './protocol/signal/fake-sender-key.js';
|
|
23
|
+
export { buildAdvSignedDeviceIdentity, generateFakePrimaryDevice } from './protocol/auth/fake-primary-device.js';
|
|
24
|
+
export { buildPairDeviceIq, buildPairSuccessIq, parsePairingQrString } from './protocol/auth/pair-device.js';
|
|
25
|
+
export { parsePreKeyUploadIq, PreKeyUploadParseError } from './protocol/signal/prekey-upload.js';
|
|
26
|
+
export { buildStreamErrorAck, buildStreamErrorCode, buildStreamErrorDeviceRemoved, buildStreamErrorReplaced, buildStreamErrorXmlNotWellFormed } from './protocol/stream/stream-error.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class WaFakeConnection {
|
|
2
|
+
constructor(id, socket) {
|
|
3
|
+
this.handlers = {};
|
|
4
|
+
this.currentState = 'open';
|
|
5
|
+
this.id = id;
|
|
6
|
+
this.socket = socket;
|
|
7
|
+
this.bindSocketEvents();
|
|
8
|
+
}
|
|
9
|
+
get state() {
|
|
10
|
+
return this.currentState;
|
|
11
|
+
}
|
|
12
|
+
setHandlers(handlers) {
|
|
13
|
+
this.handlers = handlers;
|
|
14
|
+
}
|
|
15
|
+
sendFrame(frame) {
|
|
16
|
+
if (this.currentState !== 'open') {
|
|
17
|
+
throw new Error(`cannot send frame on connection in state "${this.currentState}"`);
|
|
18
|
+
}
|
|
19
|
+
this.socket.send(frame);
|
|
20
|
+
}
|
|
21
|
+
close(code = 1000, reason = '') {
|
|
22
|
+
if (this.currentState === 'closed' || this.currentState === 'closing') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.currentState = 'closing';
|
|
26
|
+
this.socket.close(code, reason);
|
|
27
|
+
}
|
|
28
|
+
bindSocketEvents() {
|
|
29
|
+
this.socket.on('message', (data, isBinary) => {
|
|
30
|
+
if (!isBinary) {
|
|
31
|
+
this.handlers.onError?.(new Error('received unexpected text frame'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const frame = data instanceof Uint8Array
|
|
35
|
+
? data
|
|
36
|
+
: Array.isArray(data)
|
|
37
|
+
? new Uint8Array(Buffer.concat(data))
|
|
38
|
+
: new Uint8Array(data);
|
|
39
|
+
this.handlers.onFrame?.(frame);
|
|
40
|
+
});
|
|
41
|
+
this.socket.on('close', (code, reasonBuf) => {
|
|
42
|
+
this.currentState = 'closed';
|
|
43
|
+
this.handlers.onClose?.({ code, reason: reasonBuf.toString('utf8') });
|
|
44
|
+
});
|
|
45
|
+
this.socket.on('error', (error) => {
|
|
46
|
+
this.handlers.onError?.(error);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** Per-connection lifecycle pipeline (Noise handshake + stanza loop). */
|
|
2
|
+
import { buildFakeCertChain } from '../protocol/auth/cert-chain.js';
|
|
3
|
+
import { parseClientPayload } from '../protocol/auth/client-payload-validate.js';
|
|
4
|
+
import { buildSuccessNode } from '../protocol/auth/success-node.js';
|
|
5
|
+
import { decodeBinaryNodeStanza, encodeBinaryNodeStanza } from '../transport/codec.js';
|
|
6
|
+
import { X25519 } from '../transport/crypto.js';
|
|
7
|
+
import { proto } from '../transport/protos.js';
|
|
8
|
+
import { WaFakeFrameSocket } from './WaFakeFrameSocket.js';
|
|
9
|
+
import { WaFakeNoiseHandshake } from './WaFakeNoiseHandshake.js';
|
|
10
|
+
import { WaFakeTransport } from './WaFakeTransport.js';
|
|
11
|
+
const NOISE_XX_NAME = new TextEncoder().encode('Noise_XX_25519_AESGCM_SHA256\0\0\0\0');
|
|
12
|
+
const NOISE_IK_NAME = new TextEncoder().encode('Noise_IK_25519_AESGCM_SHA256\0\0\0\0');
|
|
13
|
+
const PROLOGUE = new Uint8Array([0x57, 0x41, 0x06, 0x03]);
|
|
14
|
+
export class WaFakeConnectionPipeline {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.events = {};
|
|
17
|
+
this.state = { kind: 'awaiting_prologue' };
|
|
18
|
+
this.chain = Promise.resolve();
|
|
19
|
+
this.authenticatedPayload = null;
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.frameSocket = new WaFakeFrameSocket(config.connection);
|
|
22
|
+
this.frameSocket.setHandlers({
|
|
23
|
+
onPrologue: () => this.onPrologue(),
|
|
24
|
+
onFrame: (frame) => this.scheduleFrame(frame),
|
|
25
|
+
onClose: (info) => {
|
|
26
|
+
this.state = { kind: 'closed' };
|
|
27
|
+
this.events.onClose?.(info);
|
|
28
|
+
},
|
|
29
|
+
onError: (error) => this.events.onError?.(error)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
setEvents(events) {
|
|
33
|
+
this.events = events;
|
|
34
|
+
}
|
|
35
|
+
isAuthenticated() {
|
|
36
|
+
return this.state.kind === 'authenticated';
|
|
37
|
+
}
|
|
38
|
+
/** Parsed ClientPayload of the authenticated connection; null before auth. */
|
|
39
|
+
get clientPayload() {
|
|
40
|
+
return this.authenticatedPayload;
|
|
41
|
+
}
|
|
42
|
+
async sendStanza(node) {
|
|
43
|
+
if (this.state.kind !== 'authenticated') {
|
|
44
|
+
throw new Error(`cannot send stanza while pipeline is in state "${this.state.kind}"`);
|
|
45
|
+
}
|
|
46
|
+
const stanza = encodeBinaryNodeStanza(node);
|
|
47
|
+
// Keep outbound encrypted frames in FIFO order across concurrent callers.
|
|
48
|
+
const ciphertext = await this.state.transport.encryptFrame(stanza);
|
|
49
|
+
this.frameSocket.sendFrame(ciphertext);
|
|
50
|
+
}
|
|
51
|
+
onPrologue() {
|
|
52
|
+
if (this.state.kind !== 'awaiting_prologue') {
|
|
53
|
+
this.events.onError?.(new Error('received second prologue from client'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.state = { kind: 'awaiting_client_hello' };
|
|
57
|
+
}
|
|
58
|
+
scheduleFrame(frame) {
|
|
59
|
+
this.chain = this.chain.then(() => this.handleFrame(frame));
|
|
60
|
+
}
|
|
61
|
+
async handleFrame(frame) {
|
|
62
|
+
try {
|
|
63
|
+
switch (this.state.kind) {
|
|
64
|
+
case 'awaiting_client_hello':
|
|
65
|
+
await this.handleClientHello(frame);
|
|
66
|
+
return;
|
|
67
|
+
case 'awaiting_client_finish':
|
|
68
|
+
await this.handleClientFinish(frame, this.state.handshake, this.state.serverEphemeralKeyPair);
|
|
69
|
+
return;
|
|
70
|
+
case 'authenticated':
|
|
71
|
+
await this.handleAuthenticatedFrame(frame, this.state.transport);
|
|
72
|
+
return;
|
|
73
|
+
default:
|
|
74
|
+
this.events.onError?.(new Error(`unexpected frame in state "${this.state.kind}"`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.fail(error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async handleClientHello(frame) {
|
|
82
|
+
const parsed = proto.HandshakeMessage.decode(frame);
|
|
83
|
+
const clientHello = parsed.clientHello;
|
|
84
|
+
if (!clientHello?.ephemeral) {
|
|
85
|
+
throw new Error('ClientHello missing ephemeral');
|
|
86
|
+
}
|
|
87
|
+
// IK includes encrypted static+payload in ClientHello; XX does not.
|
|
88
|
+
if (clientHello.static && clientHello.payload) {
|
|
89
|
+
await this.handleIkClientHello(clientHello);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
await this.handleXxClientHello(clientHello.ephemeral);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async handleXxClientHello(clientEphemeralPub) {
|
|
96
|
+
const handshake = new WaFakeNoiseHandshake();
|
|
97
|
+
handshake.start(NOISE_XX_NAME, PROLOGUE);
|
|
98
|
+
handshake.authenticate(clientEphemeralPub);
|
|
99
|
+
const serverEphemeral = await X25519.generateKeyPair();
|
|
100
|
+
handshake.authenticate(serverEphemeral.pubKey);
|
|
101
|
+
handshake.mixIntoKey(await X25519.scalarMult(serverEphemeral.privKey, clientEphemeralPub));
|
|
102
|
+
const encryptedServerStatic = handshake.encrypt(this.config.serverStaticKeyPair.pubKey);
|
|
103
|
+
handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
|
|
104
|
+
const certChain = await buildFakeCertChain({
|
|
105
|
+
root: this.config.rootCa,
|
|
106
|
+
leafKey: this.config.serverStaticKeyPair.pubKey
|
|
107
|
+
});
|
|
108
|
+
const encryptedCertPayload = handshake.encrypt(certChain.encoded);
|
|
109
|
+
const serverHello = proto.HandshakeMessage.encode({
|
|
110
|
+
serverHello: {
|
|
111
|
+
ephemeral: serverEphemeral.pubKey,
|
|
112
|
+
static: encryptedServerStatic,
|
|
113
|
+
payload: encryptedCertPayload
|
|
114
|
+
}
|
|
115
|
+
}).finish();
|
|
116
|
+
this.frameSocket.sendFrame(serverHello);
|
|
117
|
+
this.state = {
|
|
118
|
+
kind: 'awaiting_client_finish',
|
|
119
|
+
handshake,
|
|
120
|
+
serverEphemeralKeyPair: serverEphemeral,
|
|
121
|
+
clientEphemeralPub
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Server-side IK path used when the client already cached our static key. */
|
|
125
|
+
async handleIkClientHello(clientHello) {
|
|
126
|
+
if (!clientHello.ephemeral || !clientHello.static || !clientHello.payload) {
|
|
127
|
+
throw new Error('IK ClientHello missing ephemeral/static/payload');
|
|
128
|
+
}
|
|
129
|
+
const clientEphemeralPub = clientHello.ephemeral;
|
|
130
|
+
const encryptedClientStatic = clientHello.static;
|
|
131
|
+
const encryptedClientPayload = clientHello.payload;
|
|
132
|
+
const handshake = new WaFakeNoiseHandshake();
|
|
133
|
+
handshake.start(NOISE_IK_NAME, PROLOGUE);
|
|
134
|
+
handshake.authenticate(this.config.serverStaticKeyPair.pubKey);
|
|
135
|
+
handshake.authenticate(clientEphemeralPub);
|
|
136
|
+
handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientEphemeralPub));
|
|
137
|
+
const clientStaticKey = handshake.decrypt(encryptedClientStatic);
|
|
138
|
+
handshake.mixIntoKey(await X25519.scalarMult(this.config.serverStaticKeyPair.privKey, clientStaticKey));
|
|
139
|
+
const clientPayloadBytes = handshake.decrypt(encryptedClientPayload);
|
|
140
|
+
const clientPayload = parseClientPayload(clientPayloadBytes);
|
|
141
|
+
const serverEphemeral = await X25519.generateKeyPair();
|
|
142
|
+
handshake.authenticate(serverEphemeral.pubKey);
|
|
143
|
+
const [dhEphem, dhStatic] = await Promise.all([
|
|
144
|
+
X25519.scalarMult(serverEphemeral.privKey, clientEphemeralPub),
|
|
145
|
+
X25519.scalarMult(serverEphemeral.privKey, clientStaticKey)
|
|
146
|
+
]);
|
|
147
|
+
handshake.mixIntoKey(dhEphem);
|
|
148
|
+
handshake.mixIntoKey(dhStatic);
|
|
149
|
+
const certChain = await buildFakeCertChain({
|
|
150
|
+
root: this.config.rootCa,
|
|
151
|
+
leafKey: this.config.serverStaticKeyPair.pubKey
|
|
152
|
+
});
|
|
153
|
+
const encryptedCertPayload = handshake.encrypt(certChain.encoded);
|
|
154
|
+
const serverHello = proto.HandshakeMessage.encode({
|
|
155
|
+
serverHello: {
|
|
156
|
+
ephemeral: serverEphemeral.pubKey,
|
|
157
|
+
payload: encryptedCertPayload
|
|
158
|
+
}
|
|
159
|
+
}).finish();
|
|
160
|
+
this.frameSocket.sendFrame(serverHello);
|
|
161
|
+
await this.completeAuthentication(handshake, clientPayload, clientStaticKey);
|
|
162
|
+
}
|
|
163
|
+
async handleClientFinish(frame, handshake, serverEphemeralKeyPair) {
|
|
164
|
+
const parsed = proto.HandshakeMessage.decode(frame);
|
|
165
|
+
const clientFinish = parsed.clientFinish;
|
|
166
|
+
if (!clientFinish?.static || !clientFinish.payload) {
|
|
167
|
+
throw new Error('ClientFinish missing static/payload');
|
|
168
|
+
}
|
|
169
|
+
const clientStaticKey = handshake.decrypt(clientFinish.static);
|
|
170
|
+
handshake.mixIntoKey(await X25519.scalarMult(serverEphemeralKeyPair.privKey, clientStaticKey));
|
|
171
|
+
const clientPayloadBytes = handshake.decrypt(clientFinish.payload);
|
|
172
|
+
const clientPayload = parseClientPayload(clientPayloadBytes);
|
|
173
|
+
await this.completeAuthentication(handshake, clientPayload, clientStaticKey);
|
|
174
|
+
}
|
|
175
|
+
async completeAuthentication(handshake, clientPayload, clientStaticKey) {
|
|
176
|
+
const keys = handshake.finish();
|
|
177
|
+
const transport = new WaFakeTransport({
|
|
178
|
+
recvKey: keys.recvKey,
|
|
179
|
+
sendKey: keys.sendKey
|
|
180
|
+
});
|
|
181
|
+
this.state = { kind: 'authenticated', transport };
|
|
182
|
+
this.authenticatedPayload = clientPayload;
|
|
183
|
+
await this.sendStanza(buildSuccessNode(this.config.successNodeAttributes));
|
|
184
|
+
if (clientPayload.kind === 'login') {
|
|
185
|
+
// Real WA drains offline messages after login and closes the
|
|
186
|
+
// drain with this bulletin. Baileys-family clients buffer their
|
|
187
|
+
// events until it arrives, so send it eagerly with count=0.
|
|
188
|
+
await this.sendStanza({
|
|
189
|
+
tag: 'ib',
|
|
190
|
+
attrs: { from: 's.whatsapp.net' },
|
|
191
|
+
content: [{ tag: 'offline', attrs: { count: '0' } }]
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
this.events.onAuthenticated?.({ clientPayload, clientStaticKey });
|
|
195
|
+
}
|
|
196
|
+
async handleAuthenticatedFrame(frame, transport) {
|
|
197
|
+
const stanzaBytes = await transport.decryptFrame(frame);
|
|
198
|
+
const node = await decodeBinaryNodeStanza(stanzaBytes);
|
|
199
|
+
this.events.onStanza?.(node);
|
|
200
|
+
if (node.tag === 'iq') {
|
|
201
|
+
const response = await this.config.routeIq(node);
|
|
202
|
+
if (response !== null) {
|
|
203
|
+
await this.sendStanza(response);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
this.events.onUnhandledStanza?.(node);
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Mirror the minimal ack shape that resolves the lib pending publish.
|
|
211
|
+
if (node.tag === 'message' && node.attrs.id) {
|
|
212
|
+
const messageType = node.attrs.type ?? 'text';
|
|
213
|
+
const from = node.attrs.to ?? 's.whatsapp.net';
|
|
214
|
+
const ackAttrs = {
|
|
215
|
+
id: node.attrs.id,
|
|
216
|
+
class: 'receipt',
|
|
217
|
+
type: messageType,
|
|
218
|
+
from
|
|
219
|
+
};
|
|
220
|
+
if (node.attrs.participant) {
|
|
221
|
+
ackAttrs.participant = node.attrs.participant;
|
|
222
|
+
}
|
|
223
|
+
await this.sendStanza({
|
|
224
|
+
tag: 'ack',
|
|
225
|
+
attrs: ackAttrs
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
fail(error) {
|
|
230
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
231
|
+
this.events.onError?.(err);
|
|
232
|
+
this.state = { kind: 'closed' };
|
|
233
|
+
this.config.connection.close(1011, err.message.slice(0, 120));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** WhatsApp frame codec (prologue + 3-byte length-prefixed frames). */
|
|
2
|
+
const PROLOGUE_W = 0x57;
|
|
3
|
+
const PROLOGUE_A = 0x41;
|
|
4
|
+
const ROUTING_MAGIC = 0xed;
|
|
5
|
+
const VERSION_HEADER_LENGTH = 4;
|
|
6
|
+
const FRAME_LENGTH_PREFIX_BYTES = 3;
|
|
7
|
+
const MAX_FRAME_LENGTH = 1 << 24;
|
|
8
|
+
export class WaFakeFrameSocket {
|
|
9
|
+
constructor(connection) {
|
|
10
|
+
this.handlers = {};
|
|
11
|
+
this.inboundBuffer = new Uint8Array(0);
|
|
12
|
+
this.prologueParsed = false;
|
|
13
|
+
this.connection = connection;
|
|
14
|
+
this.connection.setHandlers({
|
|
15
|
+
onFrame: (bytes) => this.handleInboundBytes(bytes),
|
|
16
|
+
onClose: (info) => this.handlers.onClose?.(info),
|
|
17
|
+
onError: (error) => this.handlers.onError?.(error)
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
setHandlers(handlers) {
|
|
21
|
+
this.handlers = handlers;
|
|
22
|
+
}
|
|
23
|
+
sendFrame(body) {
|
|
24
|
+
if (body.byteLength >= MAX_FRAME_LENGTH) {
|
|
25
|
+
throw new Error(`frame too large: ${body.byteLength} bytes`);
|
|
26
|
+
}
|
|
27
|
+
const out = new Uint8Array(FRAME_LENGTH_PREFIX_BYTES + body.byteLength);
|
|
28
|
+
out[0] = (body.byteLength >> 16) & 0xff;
|
|
29
|
+
out[1] = (body.byteLength >> 8) & 0xff;
|
|
30
|
+
out[2] = body.byteLength & 0xff;
|
|
31
|
+
out.set(body, FRAME_LENGTH_PREFIX_BYTES);
|
|
32
|
+
this.connection.sendFrame(out);
|
|
33
|
+
}
|
|
34
|
+
handleInboundBytes(chunk) {
|
|
35
|
+
try {
|
|
36
|
+
this.inboundBuffer = concatBytes(this.inboundBuffer, chunk);
|
|
37
|
+
if (!this.prologueParsed) {
|
|
38
|
+
if (!this.tryParsePrologue()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.drainFrames();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
tryParsePrologue() {
|
|
49
|
+
const buffer = this.inboundBuffer;
|
|
50
|
+
let offset = 0;
|
|
51
|
+
let routingInfo = null;
|
|
52
|
+
if (buffer.byteLength === 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (buffer[0] === ROUTING_MAGIC) {
|
|
56
|
+
if (buffer.byteLength < 4) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const routingLen = (buffer[1] << 16) | (buffer[2] << 8) | buffer[3];
|
|
60
|
+
if (buffer.byteLength < 4 + routingLen + VERSION_HEADER_LENGTH) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
routingInfo = buffer.slice(4, 4 + routingLen);
|
|
64
|
+
offset = 4 + routingLen;
|
|
65
|
+
}
|
|
66
|
+
else if (buffer.byteLength < VERSION_HEADER_LENGTH) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const versionStart = offset;
|
|
70
|
+
const w = buffer[versionStart];
|
|
71
|
+
const a = buffer[versionStart + 1];
|
|
72
|
+
if (w !== PROLOGUE_W || a !== PROLOGUE_A) {
|
|
73
|
+
throw new Error(`invalid prologue magic: expected "WA", got ${hex(w)} ${hex(a)}`);
|
|
74
|
+
}
|
|
75
|
+
const protocolVersion = buffer[versionStart + 2];
|
|
76
|
+
const dictVersion = buffer[versionStart + 3];
|
|
77
|
+
offset = versionStart + VERSION_HEADER_LENGTH;
|
|
78
|
+
this.inboundBuffer = buffer.slice(offset);
|
|
79
|
+
this.prologueParsed = true;
|
|
80
|
+
this.handlers.onPrologue?.({ protocolVersion, dictVersion, routingInfo });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
drainFrames() {
|
|
84
|
+
while (true) {
|
|
85
|
+
const buffer = this.inboundBuffer;
|
|
86
|
+
if (buffer.byteLength < FRAME_LENGTH_PREFIX_BYTES) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const length = (buffer[0] << 16) | (buffer[1] << 8) | buffer[2];
|
|
90
|
+
if (length >= MAX_FRAME_LENGTH) {
|
|
91
|
+
throw new Error(`frame too large: ${length} bytes`);
|
|
92
|
+
}
|
|
93
|
+
const total = FRAME_LENGTH_PREFIX_BYTES + length;
|
|
94
|
+
if (buffer.byteLength < total) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const body = buffer.slice(FRAME_LENGTH_PREFIX_BYTES, total);
|
|
98
|
+
this.inboundBuffer = buffer.slice(total);
|
|
99
|
+
this.handlers.onFrame?.(body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function concatBytes(a, b) {
|
|
104
|
+
if (a.byteLength === 0)
|
|
105
|
+
return b;
|
|
106
|
+
if (b.byteLength === 0)
|
|
107
|
+
return a;
|
|
108
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
109
|
+
out.set(a, 0);
|
|
110
|
+
out.set(b, a.byteLength);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function hex(byte) {
|
|
114
|
+
if (byte === undefined)
|
|
115
|
+
return 'undefined';
|
|
116
|
+
return `0x${byte.toString(16).padStart(2, '0')}`;
|
|
117
|
+
}
|