diff --git a/modules/service_plugins/packet_capture_service.py b/modules/service_plugins/packet_capture_service.py index 5fac5ec..56fce86 100644 --- a/modules/service_plugins/packet_capture_service.py +++ b/modules/service_plugins/packet_capture_service.py @@ -76,10 +76,23 @@ NEIGHBORS_ATTEMPT_STATE_KEY = "packet_capture.last_neighbors_attempt" # instead, which is longer. NEIGHBORS_RETRY_BACKOFF_SECONDS = 300.0 +# waev.app rejects a JWT whose exp is more than an hour past its iat, so the +# project-wide 24-hour default cannot authenticate there (#248). These are the +# values its brokers accept, applied only when nothing else was configured; +# renewal is set just under the TTL so a token is replaced before it expires. +WAEV_JWT_TTL_SECONDS = 3600 +WAEV_JWT_RENEWAL_INTERVAL = 3500 + # Sentinel meaning "no IATA configured" (documented as invalid in config.ini.example). DEFAULT_IATA = "XYZ" +def _is_waev_host(host: str) -> bool: + """Whether ``host`` is a waev.app broker (the apex or any subdomain).""" + normalized = (host or "").strip().lower().rstrip(".") + return normalized == "waev.app" or normalized.endswith(".waev.app") + + def _decode_key_str(key_str: str) -> Optional[bytes]: """Decode a 16-byte channel key from a hex (32 chars) or base64 string.""" key_str = key_str.strip() @@ -689,6 +702,8 @@ class PacketCaptureService(BaseServicePlugin): global_jwt_renewal = config.getint("PacketCapture", "jwt_renewal_interval", fallback=43200) global_jwt_ttl = config.getint("PacketCapture", "jwt_ttl_seconds", fallback=86400) + global_renewal_set = config.has_option("PacketCapture", "jwt_renewal_interval") + global_ttl_set = config.has_option("PacketCapture", "jwt_ttl_seconds") # Parse multiple brokers (mqtt1_*, mqtt2_*, etc.) broker_num = 1 @@ -712,21 +727,42 @@ class PacketCaptureService(BaseServicePlugin): if not upload_packet_types: upload_packet_types = None + host = config.get("PacketCapture", server_key, fallback="localhost") + # waev.app refuses a token whose lifetime runs past an hour, so the + # 24-hour default never authenticates there and the operator only sees + # a bare auth failure (#248). Give those brokers a lifetime they accept + # unless a value was chosen for them, per broker or globally. + waev_default = _is_waev_host(host) + renew_key = f"mqtt{broker_num}_jwt_renewal_interval" if config.has_option("PacketCapture", renew_key): jwt_renewal_interval = config.getint("PacketCapture", renew_key) + elif waev_default and not global_renewal_set: + jwt_renewal_interval = WAEV_JWT_RENEWAL_INTERVAL else: jwt_renewal_interval = global_jwt_renewal ttl_key = f"mqtt{broker_num}_jwt_ttl_seconds" if config.has_option("PacketCapture", ttl_key): jwt_ttl_seconds = config.getint("PacketCapture", ttl_key) + elif waev_default and not global_ttl_set: + jwt_ttl_seconds = WAEV_JWT_TTL_SECONDS else: jwt_ttl_seconds = global_jwt_ttl + if waev_default and ( + jwt_ttl_seconds != global_jwt_ttl or jwt_renewal_interval != global_jwt_renewal + ): + self.logger.info( + "MQTT broker %d (%s) is a waev.app host; using a %ds JWT TTL and %ds " + "renewal instead of the defaults, which it rejects. Set " + "mqtt%d_jwt_ttl_seconds to override.", + broker_num, host, jwt_ttl_seconds, jwt_renewal_interval, broker_num, + ) + broker = { "enabled": True, - "host": config.get("PacketCapture", server_key, fallback="localhost"), + "host": host, "port": config.getint("PacketCapture", f"mqtt{broker_num}_port", fallback=1883), "username": config.get("PacketCapture", f"mqtt{broker_num}_username", fallback=None), "password": config.get("PacketCapture", f"mqtt{broker_num}_password", fallback=None), diff --git a/tests/unit/test_packet_capture_mqtt_jwt_config.py b/tests/unit/test_packet_capture_mqtt_jwt_config.py index 899a2d5..54a1960 100644 --- a/tests/unit/test_packet_capture_mqtt_jwt_config.py +++ b/tests/unit/test_packet_capture_mqtt_jwt_config.py @@ -124,3 +124,81 @@ def test_auth_token_iat_exp_uses_broker_then_global(): ) def test_jwt_ttl_log_phrase(ttl: int, phrase: str): assert PacketCaptureService._jwt_ttl_log_phrase(ttl) == phrase + + +def _waev_service(ini: str): + bot = _bot_from_ini(ini) + svc = object.__new__(PacketCaptureService) + svc.bot = bot + svc.logger = MagicMock() + return svc, PacketCaptureService._parse_mqtt_brokers(svc, bot.config) + + +def test_waev_broker_defaults_to_a_ttl_the_broker_accepts(): + """waev.app rejects the 24-hour default, leaving the bot unable to auth (#248).""" + _, brokers = _waev_service( + """ + [PacketCapture] + enabled = false + mqtt1_server = mqtt-a.waev.app + mqtt2_server = broker.example + """ + ) + assert brokers[0]["jwt_ttl_seconds"] == 3600 + assert brokers[0]["jwt_renewal_interval"] == 3500 + # Only waev hosts move; every other broker keeps the project default. + assert brokers[1]["jwt_ttl_seconds"] == 86400 + assert brokers[1]["jwt_renewal_interval"] == 43200 + + +@pytest.mark.parametrize("host", ["waev.app", "WAEV.APP", "mqtt-b.waev.app"]) +def test_waev_default_matches_apex_and_subdomains(host: str): + _, brokers = _waev_service( + f""" + [PacketCapture] + enabled = false + mqtt1_server = {host} + """ + ) + assert brokers[0]["jwt_ttl_seconds"] == 3600 + + +@pytest.mark.parametrize("host", ["notwaev.app", "waev.app.example.com"]) +def test_waev_default_does_not_match_lookalike_hosts(host: str): + _, brokers = _waev_service( + f""" + [PacketCapture] + enabled = false + mqtt1_server = {host} + """ + ) + assert brokers[0]["jwt_ttl_seconds"] == 86400 + + +def test_explicit_per_broker_ttl_beats_the_waev_default(): + _, brokers = _waev_service( + """ + [PacketCapture] + enabled = false + mqtt1_server = mqtt-a.waev.app + mqtt1_jwt_ttl_seconds = 1800 + mqtt1_jwt_renewal_interval = 900 + """ + ) + assert brokers[0]["jwt_ttl_seconds"] == 1800 + assert brokers[0]["jwt_renewal_interval"] == 900 + + +def test_explicit_global_ttl_beats_the_waev_default(): + """An operator who set a global value meant it, even for waev.""" + _, brokers = _waev_service( + """ + [PacketCapture] + enabled = false + jwt_ttl_seconds = 7200 + jwt_renewal_interval = 3600 + mqtt1_server = mqtt-a.waev.app + """ + ) + assert brokers[0]["jwt_ttl_seconds"] == 7200 + assert brokers[0]["jwt_renewal_interval"] == 3600