fix: secure whatsapp bridge with automatic local auth token
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import OrderedDict
|
||||
@@ -29,6 +30,29 @@ class WhatsAppConfig(Base):
|
||||
group_policy: Literal["open", "mention"] = "open" # "open" responds to all, "mention" only when @mentioned
|
||||
|
||||
|
||||
def _bridge_token_path() -> Path:
|
||||
from nanobot.config.paths import get_runtime_subdir
|
||||
|
||||
return get_runtime_subdir("whatsapp-auth") / "bridge-token"
|
||||
|
||||
|
||||
def _load_or_create_bridge_token(path: Path) -> str:
|
||||
"""Load a persisted bridge token or create one on first use."""
|
||||
if path.exists():
|
||||
token = path.read_text(encoding="utf-8").strip()
|
||||
if token:
|
||||
return token
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
token = secrets.token_urlsafe(32)
|
||||
path.write_text(token, encoding="utf-8")
|
||||
try:
|
||||
path.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return token
|
||||
|
||||
|
||||
class WhatsAppChannel(BaseChannel):
|
||||
"""
|
||||
WhatsApp channel that connects to a Node.js bridge.
|
||||
@@ -51,6 +75,18 @@ class WhatsAppChannel(BaseChannel):
|
||||
self._ws = None
|
||||
self._connected = False
|
||||
self._processed_message_ids: OrderedDict[str, None] = OrderedDict()
|
||||
self._bridge_token: str | None = None
|
||||
|
||||
def _effective_bridge_token(self) -> str:
|
||||
"""Resolve the bridge token, generating a local secret when needed."""
|
||||
if self._bridge_token is not None:
|
||||
return self._bridge_token
|
||||
configured = self.config.bridge_token.strip()
|
||||
if configured:
|
||||
self._bridge_token = configured
|
||||
else:
|
||||
self._bridge_token = _load_or_create_bridge_token(_bridge_token_path())
|
||||
return self._bridge_token
|
||||
|
||||
async def login(self, force: bool = False) -> bool:
|
||||
"""
|
||||
@@ -60,8 +96,6 @@ class WhatsAppChannel(BaseChannel):
|
||||
authentication flow. The process blocks until the user scans the QR code
|
||||
or interrupts with Ctrl+C.
|
||||
"""
|
||||
from nanobot.config.paths import get_runtime_subdir
|
||||
|
||||
try:
|
||||
bridge_dir = _ensure_bridge_setup()
|
||||
except RuntimeError as e:
|
||||
@@ -69,9 +103,8 @@ class WhatsAppChannel(BaseChannel):
|
||||
return False
|
||||
|
||||
env = {**os.environ}
|
||||
if self.config.bridge_token:
|
||||
env["BRIDGE_TOKEN"] = self.config.bridge_token
|
||||
env["AUTH_DIR"] = str(get_runtime_subdir("whatsapp-auth"))
|
||||
env["BRIDGE_TOKEN"] = self._effective_bridge_token()
|
||||
env["AUTH_DIR"] = str(_bridge_token_path().parent)
|
||||
|
||||
logger.info("Starting WhatsApp bridge for QR login...")
|
||||
try:
|
||||
@@ -97,11 +130,9 @@ class WhatsAppChannel(BaseChannel):
|
||||
try:
|
||||
async with websockets.connect(bridge_url) as ws:
|
||||
self._ws = ws
|
||||
# Send auth token if configured
|
||||
if self.config.bridge_token:
|
||||
await ws.send(
|
||||
json.dumps({"type": "auth", "token": self.config.bridge_token})
|
||||
)
|
||||
await ws.send(
|
||||
json.dumps({"type": "auth", "token": self._effective_bridge_token()})
|
||||
)
|
||||
self._connected = True
|
||||
logger.info("Connected to WhatsApp bridge")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user