Skip to content

Commit 8ef4a70

Browse files
committed
Add RawNodeHandler hook and DisabledFeatures policy
Adds two opt-in extension points for proxy/intermediary use cases: 1. `Client.RawNodeHandler` — called for every inbound node after Noise decrypt + Node decode, before standard dispatch (IQ response match, tag-based routing). Handlers can drop the node, replace it with a modified copy, or pass through. Lets an external system observe or take over inbound stanza handling without forking the library. 2. `Client.DisabledFeatures.Signal` — short-circuits the library's Signal session machinery: decryptMessages becomes a true no-op (no decrypt and no ack — the downstream decrypting client is responsible for ack'ing), uploadPreKeys becomes a no-op. Use when an external system owns the Signal session for this device. Both are labeled DANGEROUS in their godoc: incorrect use breaks stateful invariants (session ratchets, IQ correlation, etc.). They exist to let upstream services hold the Signal session externally and forward raw envelopes verbatim, without the library second-guessing. Designed as forward-compatible additions: - DisabledFeatures is a struct so future flags add fields rather than parameters. - RawNodeHandler returns (modified, drop) so the contract can grow without changing the signature. Zero behavior change when the new fields are left at their zero values.
1 parent 3b5b4fe commit 8ef4a70

3 files changed

Lines changed: 62 additions & 0 deletions

File tree

client.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,41 @@ type EventHandler func(evt any)
4646
type EventHandlerWithSuccessStatus func(evt any) bool
4747
type nodeHandler func(ctx context.Context, node *waBinary.Node)
4848

49+
// RawNodeHandler is called for every inbound stanza node after Noise
50+
// transport decryption and binary decoding, but before standard
51+
// dispatch (tag-based routing, IQ response correlation).
52+
//
53+
// If the handler returns drop=true, the node is not dispatched further
54+
// (no tag handler runs, no IQ response is matched). If the handler
55+
// returns a non-nil modified node, it replaces the original for the
56+
// rest of the dispatch path.
57+
//
58+
// DANGEROUS: this is a low-level hook. Returning incorrect values
59+
// breaks Signal session continuity, IQ response correlation, app state
60+
// invariants, and other stateful parts of the protocol. Intended for
61+
// proxy implementations where an external system owns part of the
62+
// protocol state machine (see also [DisabledFeatures]).
63+
type RawNodeHandler func(ctx context.Context, node *waBinary.Node) (modified *waBinary.Node, drop bool)
64+
65+
// DisabledFeatures lets callers turn off built-in whatsmeow processing
66+
// paths. Used by proxies where an external system owns parts of the
67+
// protocol state, e.g. an upstream service that holds the Signal
68+
// session and handles its own decryption.
69+
//
70+
// DANGEROUS: each flag disables a piece of state machinery the rest of
71+
// the library assumes is running. Toggle deliberately; the surrounding
72+
// code does not paper over the resulting gaps.
73+
type DisabledFeatures struct {
74+
// Signal disables the library's Signal-session machinery. When set:
75+
// - Incoming `<message>` envelopes are not decrypted and not
76+
// ack'd. The actual decrypting client downstream is responsible
77+
// for ack'ing once it has processed the message.
78+
// - The periodic prekey-upload loop is a no-op.
79+
// Combine with [RawNodeHandler] to forward the raw envelopes to the
80+
// system that actually owns the Signal session.
81+
Signal bool
82+
}
83+
4984
var nextHandlerID uint32
5085

5186
type wrappedEventHandler struct {
@@ -177,6 +212,14 @@ type Client struct {
177212
// If false, decrypting a message from untrusted devices will fail.
178213
AutoTrustIdentity bool
179214

215+
// RawNodeHandler, if non-nil, is called for every inbound node
216+
// after decoding but before standard dispatch. See [RawNodeHandler].
217+
RawNodeHandler RawNodeHandler
218+
219+
// DisabledFeatures controls which built-in processing paths are
220+
// skipped. See [DisabledFeatures].
221+
DisabledFeatures DisabledFeatures
222+
180223
// Should SubscribePresence return an error if no privacy token is stored for the user?
181224
ErrorOnSubscribePresenceWithoutToken bool
182225

@@ -826,6 +869,16 @@ func (cli *Client) handleFrame(ctx context.Context, data []byte) {
826869
cli.Log.Debugf("Errored frame hex: %s", hex.EncodeToString(decompressed))
827870
return
828871
}
872+
if h := cli.RawNodeHandler; h != nil {
873+
modified, drop := h(ctx, node)
874+
if drop {
875+
cli.recvLog.Debugf("RawNodeHandler dropped node: %s", node.XMLString())
876+
return
877+
}
878+
if modified != nil {
879+
node = modified
880+
}
881+
}
829882
cli.recvLog.Debugf("%s", node.XMLString())
830883
if node.Tag == "xmlstreamend" {
831884
if !cli.isExpectedDisconnect() {

message.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ func (cli *Client) handleEncryptedMessage(ctx context.Context, node *waBinary.No
6060
var cancelled bool
6161
defer cli.maybeDeferredAck(ctx, node)(&cancelled)
6262
cancelled = cli.handlePlaintextMessage(ctx, info, node)
63+
} else if cli.DisabledFeatures.Signal {
64+
// External system owns the Signal session. We do not decrypt
65+
// and we do not ack: the actual decrypting client downstream
66+
// is responsible for ack'ing once it has received and
67+
// processed the message.
6368
} else {
6469
cli.decryptMessages(ctx, info, node)
6570
}

prekeys.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ func (cli *Client) getServerPreKeyCount(ctx context.Context) (int, error) {
4848
}
4949

5050
func (cli *Client) uploadPreKeys(ctx context.Context, initialUpload bool) {
51+
if cli.DisabledFeatures.Signal {
52+
cli.Log.Debugf("Prekey upload skipped (DisabledFeatures.Signal)")
53+
return
54+
}
5155
cli.uploadPreKeysLock.Lock()
5256
defer cli.uploadPreKeysLock.Unlock()
5357
if cli.lastPreKeyUpload.Add(10 * time.Minute).After(time.Now()) {

0 commit comments

Comments
 (0)