mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(gateway): dual-stack webhook bind for wecom/msgraph/whatsapp_cloud/teams/telegram siblings
Same class of bug as the LINE adapter (NS-603): defaulting the webhook bind to "0.0.0.0" (or hardcoding it) binds IPv4 ONLY, so the listener is unreachable over IPv6-only private networks such as Fly.io 6PN. - wecom callback_adapter: DEFAULT_HOST None; config.py env seed no longer forces 0.0.0.0 when WECOM_CALLBACK_HOST is unset. - msgraph_webhook: DEFAULT_HOST None; the allowed_source_cidrs requirement still fires for the all-interfaces default (host=None is treated as network-accessible). - whatsapp_cloud: DEFAULT_WEBHOOK_HOST None. - teams: hardcoded 0.0.0.0 TCPSite bind → _DEFAULT_HOST=None with new TEAMS_HOST / extra.host override (mirrors LINE_HOST pattern). - telegram: hardcoded listen="0.0.0.0" → default "" (tornado bind_sockets opens one socket per address family; verified against PTB 22.6/tornado) with new TELEGRAM_WEBHOOK_HOST / extra.webhook_host override. Explicit host overrides everywhere are preserved; empty/unset collapses to the dual-stack default. "::" remains a bad substitute on bindv6only=1 hosts (see LINE adapter comment).
This commit is contained in:
parent
cf1e3585b6
commit
2c771be406
7 changed files with 72 additions and 14 deletions
|
|
@ -2323,7 +2323,10 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
|||
"agent_id": getenv("WECOM_CALLBACK_AGENT_ID", ""),
|
||||
"token": getenv("WECOM_CALLBACK_TOKEN", ""),
|
||||
"encoding_aes_key": getenv("WECOM_CALLBACK_ENCODING_AES_KEY", ""),
|
||||
"host": getenv("WECOM_CALLBACK_HOST", "0.0.0.0"),
|
||||
# No default here: an unset WECOM_CALLBACK_HOST leaves extra.host
|
||||
# falsy so the adapter's dual-stack DEFAULT_HOST=None applies
|
||||
# (binds IPv4 + IPv6; "0.0.0.0" was IPv4-only, NS-603).
|
||||
"host": getenv("WECOM_CALLBACK_HOST", ""),
|
||||
"port": getenv_int("WECOM_CALLBACK_PORT", 8645),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,13 @@ from gateway.platforms.base import (
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
# ``None`` → aiohttp/asyncio ``create_server`` binds one listening socket per
|
||||
# address family (IPv4 + IPv6). The old "0.0.0.0" default bound IPv4 ONLY and
|
||||
# was unreachable over IPv6-only private networks (e.g. Fly.io 6PN) — same
|
||||
# bug as the LINE adapter (NS-603) and gateway/platforms/webhook.py
|
||||
# (d542894ad). Pin a host via extra.host. The all-interfaces default still
|
||||
# requires extra.allowed_source_cidrs (see _source_allowlist_required_but_missing).
|
||||
DEFAULT_HOST = None
|
||||
DEFAULT_PORT = 8646
|
||||
DEFAULT_WEBHOOK_PATH = "/msgraph/webhook"
|
||||
DEFAULT_MAX_SEEN_RECEIPTS = 5000
|
||||
|
|
@ -49,7 +55,9 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
|
|||
def __init__(self, config: PlatformConfig):
|
||||
super().__init__(config, Platform.MSGRAPH_WEBHOOK)
|
||||
extra = config.extra or {}
|
||||
self._host: str = str(extra.get("host", DEFAULT_HOST))
|
||||
# Falsy host (None/"") collapses to the dual-stack default.
|
||||
_raw_host = extra.get("host", DEFAULT_HOST) or DEFAULT_HOST
|
||||
self._host: Optional[str] = str(_raw_host) if _raw_host else None
|
||||
self._port: int = int(extra.get("port", DEFAULT_PORT))
|
||||
self._webhook_path: str = self._normalize_path(
|
||||
extra.get("webhook_path", DEFAULT_WEBHOOK_PATH)
|
||||
|
|
@ -138,7 +146,9 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
|
|||
self._notification_scheduler = scheduler
|
||||
|
||||
def _source_allowlist_required_but_missing(self) -> bool:
|
||||
return is_network_accessible(self._host) and not self._allowed_source_networks
|
||||
# host=None binds all interfaces (both families) — network-accessible.
|
||||
host_is_public = self._host is None or is_network_accessible(self._host)
|
||||
return host_is_public and not self._allowed_source_networks
|
||||
|
||||
async def connect(self, *, is_reconnect: bool = False) -> bool:
|
||||
if self._client_state is None:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Optional / Phase-3+:
|
|||
- WHATSAPP_CLOUD_APP_SECRET (HMAC key for X-Hub-Signature-256)
|
||||
- WHATSAPP_CLOUD_WABA_ID (analytics / future use)
|
||||
- WHATSAPP_CLOUD_VERIFY_TOKEN (hub.verify_token shared secret)
|
||||
- WHATSAPP_CLOUD_WEBHOOK_HOST (default 0.0.0.0)
|
||||
- WHATSAPP_CLOUD_WEBHOOK_HOST (default: unset → dual-stack, all interfaces IPv4+IPv6)
|
||||
- WHATSAPP_CLOUD_WEBHOOK_PORT (default 8090)
|
||||
- WHATSAPP_CLOUD_WEBHOOK_PATH (default /whatsapp/webhook)
|
||||
- WHATSAPP_CLOUD_API_VERSION (default v20.0)
|
||||
|
|
@ -86,7 +86,12 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
DEFAULT_API_VERSION = "v20.0"
|
||||
DEFAULT_WEBHOOK_HOST = "0.0.0.0"
|
||||
# ``None`` → aiohttp/asyncio ``create_server`` binds one listening socket per
|
||||
# address family (IPv4 + IPv6). The old "0.0.0.0" default bound IPv4 ONLY and
|
||||
# was unreachable over IPv6-only private networks (e.g. Fly.io 6PN) — same
|
||||
# bug as the LINE adapter (NS-603) and gateway/platforms/webhook.py
|
||||
# (d542894ad). Pin a host via WHATSAPP_CLOUD_WEBHOOK_HOST or extra.webhook_host.
|
||||
DEFAULT_WEBHOOK_HOST = None
|
||||
DEFAULT_WEBHOOK_PORT = 8090
|
||||
DEFAULT_WEBHOOK_PATH = "/whatsapp/webhook"
|
||||
GRAPH_API_BASE = "https://graph.facebook.com"
|
||||
|
|
@ -217,7 +222,11 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
|
|||
self._verify_token: str = str(extra.get("verify_token", "")).strip()
|
||||
|
||||
# Webhook server config
|
||||
self._webhook_host: str = str(extra.get("webhook_host", DEFAULT_WEBHOOK_HOST))
|
||||
# Falsy host (None/"") collapses to the dual-stack default.
|
||||
_raw_webhook_host = extra.get("webhook_host", DEFAULT_WEBHOOK_HOST) or DEFAULT_WEBHOOK_HOST
|
||||
self._webhook_host: Optional[str] = (
|
||||
str(_raw_webhook_host) if _raw_webhook_host else None
|
||||
)
|
||||
self._webhook_port: int = int(extra.get("webhook_port", DEFAULT_WEBHOOK_PORT))
|
||||
self._webhook_path: str = self._normalize_path(
|
||||
extra.get("webhook_path", DEFAULT_WEBHOOK_PATH)
|
||||
|
|
|
|||
|
|
@ -105,6 +105,12 @@ _DEFAULT_PORT = 3978
|
|||
# Bot Framework activities are JSON payloads well under 1 MiB; an explicit
|
||||
# aiohttp client_max_size keeps oversized/chunked request bodies bounded.
|
||||
_MAX_BODY_BYTES = 1_048_576
|
||||
# ``None`` → aiohttp/asyncio ``create_server`` binds one listening socket per
|
||||
# address family (IPv4 + IPv6). The old hardcoded "0.0.0.0" bound IPv4 ONLY
|
||||
# and was unreachable over IPv6-only private networks (e.g. Fly.io 6PN) —
|
||||
# same bug as the LINE adapter (NS-603) and gateway/platforms/webhook.py
|
||||
# (d542894ad). Pin a host via TEAMS_HOST or extra.host.
|
||||
_DEFAULT_HOST = None
|
||||
_WEBHOOK_PATH = "/api/messages"
|
||||
|
||||
|
||||
|
|
@ -705,6 +711,9 @@ class TeamsAdapter(BasePlatformAdapter):
|
|||
self._port = _coerce_port(
|
||||
extra.get("port") or os.getenv("TEAMS_PORT", str(_DEFAULT_PORT))
|
||||
)
|
||||
# Falsy host (unset/"") collapses to the dual-stack default (None).
|
||||
_raw_host = extra.get("host") or os.getenv("TEAMS_HOST", "") or _DEFAULT_HOST
|
||||
self._host: Optional[str] = str(_raw_host) if _raw_host else None
|
||||
self._app: Optional["App"] = None
|
||||
self._runner: Optional["web.AppRunner"] = None
|
||||
self._dedup = MessageDeduplicator(max_size=1000)
|
||||
|
|
@ -774,13 +783,14 @@ class TeamsAdapter(BasePlatformAdapter):
|
|||
|
||||
self._runner = web.AppRunner(aiohttp_app)
|
||||
await self._runner.setup()
|
||||
site = web.TCPSite(self._runner, "0.0.0.0", self._port)
|
||||
site = web.TCPSite(self._runner, self._host, self._port)
|
||||
await site.start()
|
||||
|
||||
self._running = True
|
||||
self._mark_connected()
|
||||
logger.info(
|
||||
"[teams] Webhook server listening on 0.0.0.0:%d%s",
|
||||
"[teams] Webhook server listening on %s:%d%s",
|
||||
self._host or "* (all interfaces, IPv4+IPv6)",
|
||||
self._port,
|
||||
_WEBHOOK_PATH,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ optional_env:
|
|||
description: "Webhook listen port (Bot Framework default: 3978)"
|
||||
prompt: "Webhook port"
|
||||
password: false
|
||||
- name: TEAMS_HOST
|
||||
description: "Webhook bind host (default: unset → dual-stack, all interfaces IPv4+IPv6)"
|
||||
prompt: "Webhook host"
|
||||
password: false
|
||||
- name: TEAMS_ALLOWED_USERS
|
||||
description: "Comma-separated Teams user IDs / UPNs allowed to talk to the bot"
|
||||
prompt: "Allowed users (comma-separated)"
|
||||
|
|
|
|||
|
|
@ -3592,6 +3592,8 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
|
||||
TELEGRAM_WEBHOOK_URL Public HTTPS URL (e.g. https://app.fly.dev/telegram)
|
||||
TELEGRAM_WEBHOOK_PORT Local listen port (default 8443)
|
||||
TELEGRAM_WEBHOOK_HOST Bind host (default: unset → dual-stack,
|
||||
all interfaces IPv4+IPv6)
|
||||
TELEGRAM_WEBHOOK_SECRET Secret token for update verification
|
||||
"""
|
||||
# Explicit connect() is the only operation allowed to reopen polling
|
||||
|
|
@ -3944,6 +3946,16 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
# start rather than silently run in fail-open mode.
|
||||
# See GHSA-3vpc-7q5r-276h.
|
||||
webhook_port = env_int("TELEGRAM_WEBHOOK_PORT", 8443)
|
||||
# Bind host. Default "" → tornado bind_sockets opens one
|
||||
# listening socket per address family (IPv4 + IPv6). The old
|
||||
# hardcoded "0.0.0.0" bound IPv4 ONLY and was unreachable
|
||||
# over IPv6-only private networks (e.g. Fly.io 6PN) — same
|
||||
# bug as the LINE adapter (NS-603). Pin via
|
||||
# TELEGRAM_WEBHOOK_HOST or platforms.telegram.extra.webhook_host.
|
||||
webhook_host = (
|
||||
os.getenv("TELEGRAM_WEBHOOK_HOST", "").strip()
|
||||
or str((self.config.extra or {}).get("webhook_host") or "").strip()
|
||||
)
|
||||
webhook_secret = os.getenv("TELEGRAM_WEBHOOK_SECRET", "").strip()
|
||||
if not webhook_secret:
|
||||
raise RuntimeError(
|
||||
|
|
@ -3962,7 +3974,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
webhook_path = urlparse(webhook_url).path or "/telegram"
|
||||
|
||||
await self._app.updater.start_webhook(
|
||||
listen="0.0.0.0",
|
||||
listen=webhook_host,
|
||||
port=webhook_port,
|
||||
url_path=webhook_path,
|
||||
webhook_url=webhook_url,
|
||||
|
|
@ -3978,8 +3990,11 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
self._polling_progress_accepting = False
|
||||
self._send_path_degraded = False
|
||||
logger.info(
|
||||
"[%s] Webhook server listening on 0.0.0.0:%d%s",
|
||||
self.name, webhook_port, webhook_path,
|
||||
"[%s] Webhook server listening on %s:%d%s",
|
||||
self.name,
|
||||
webhook_host or "* (all interfaces, IPv4+IPv6)",
|
||||
webhook_port,
|
||||
webhook_path,
|
||||
)
|
||||
else:
|
||||
# ── Polling mode (default) ───────────────────────────
|
||||
|
|
|
|||
|
|
@ -51,7 +51,12 @@ from plugins.platforms.wecom.wecom_crypto import WXBizMsgCrypt, WeComCryptoError
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
# ``None`` → aiohttp/asyncio ``create_server`` binds one listening socket per
|
||||
# address family (IPv4 + IPv6). The old "0.0.0.0" default bound IPv4 ONLY and
|
||||
# was unreachable over IPv6-only private networks (e.g. Fly.io 6PN) — same
|
||||
# bug as the LINE adapter (NS-603) and gateway/platforms/webhook.py
|
||||
# (d542894ad). Pin a host via WECOM_CALLBACK_HOST or extra.host.
|
||||
DEFAULT_HOST = None
|
||||
DEFAULT_PORT = 8645
|
||||
DEFAULT_PATH = "/wecom/callback"
|
||||
# Cap pre-auth request bodies. WeCom callbacks are small encrypted XML
|
||||
|
|
@ -71,7 +76,9 @@ class WecomCallbackAdapter(BasePlatformAdapter):
|
|||
def __init__(self, config: PlatformConfig):
|
||||
super().__init__(config, Platform.WECOM_CALLBACK)
|
||||
extra = config.extra or {}
|
||||
self._host = str(extra.get("host") or DEFAULT_HOST)
|
||||
# Falsy host (None/"") collapses to the dual-stack default.
|
||||
_raw_host = extra.get("host") or DEFAULT_HOST
|
||||
self._host = str(_raw_host) if _raw_host else None
|
||||
self._port = int(extra.get("port") or DEFAULT_PORT)
|
||||
self._path = str(extra.get("path") or DEFAULT_PATH)
|
||||
self._apps: List[Dict[str, Any]] = self._normalize_apps(extra)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue