From 7ef294fd65951b33b8c4c91c69080708aa16d9ed Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 8 Jun 2026 13:38:51 -0400 Subject: [PATCH] fix: fall back to lan mode in pic_ngo Caddyfile when token is empty On a fresh install before DDNS registration completes, ddns.token is empty. Writing `token ` (bare keyword, no value) causes Caddy to reject the Caddyfile at startup with "wrong argument count or unexpected line ending after 'token'". Guard added: if the token is empty, generate a LAN-mode Caddyfile so Caddy starts cleanly. The Caddyfile is regenerated automatically once registration completes and the token is persisted to cell_config.json. Co-Authored-By: Claude Sonnet 4.6 --- api/caddy_manager.py | 12 ++++++++++++ tests/test_caddy_manager.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/api/caddy_manager.py b/api/caddy_manager.py index 7912b35..dcb6154 100644 --- a/api/caddy_manager.py +++ b/api/caddy_manager.py @@ -319,6 +319,18 @@ class CaddyManager(BaseServiceManager): # Strip legacy /api/v1 suffix — the pic_ngo plugin appends /api/v1 itself. ddns_api = _raw_api.rstrip('/').removesuffix('/api/v1') + # No token yet (fresh install, pre-registration) — Caddy would reject a + # bare `token` keyword with no value. Fall back to LAN mode so Caddy + # starts cleanly; the Caddyfile is regenerated once registration completes. + if not ddns_token: + logger.warning( + 'pic_ngo mode configured but no DDNS token available; ' + 'falling back to lan mode until registration completes' + ) + cert_path, key_path = self._tls_cert_pair() + return self._caddyfile_lan(cell_name, service_routes, core_routes, + cert_path, key_path) + return ( f"{self._global_acme_block(email)}\n" "\n" diff --git a/tests/test_caddy_manager.py b/tests/test_caddy_manager.py index ac48074..3708d0b 100644 --- a/tests/test_caddy_manager.py +++ b/tests/test_caddy_manager.py @@ -640,6 +640,31 @@ class TestCaddyfileLanCustomCert(unittest.TestCase): self.assertNotIn('/etc/caddy/internal/cert.pem', out) +class TestPicNgoNoTokenFallback(unittest.TestCase): + """pic_ngo mode with no token falls back to lan so Caddy starts cleanly.""" + + def test_empty_token_generates_lan_caddyfile(self): + mgr = _mgr() + mgr.config_manager.configs = {'ddns': {'token': '', 'url': 'https://ddns.pic.ngo'}} + with patch.dict(os.environ, {}, clear=False): + os.environ.pop('DDNS_TOKEN', None) + os.environ.pop('DDNS_URL', None) + out = mgr.generate_caddyfile({'cell_name': 'x', 'domain_mode': 'pic_ngo'}, []) + self.assertIn('auto_https off', out) + self.assertNotIn('dns pic_ngo', out) + self.assertNotIn('token', out) + + def test_missing_ddns_config_generates_lan_caddyfile(self): + mgr = _mgr() + mgr.config_manager.configs = {} + with patch.dict(os.environ, {}, clear=False): + os.environ.pop('DDNS_TOKEN', None) + os.environ.pop('DDNS_URL', None) + out = mgr.generate_caddyfile({'cell_name': 'x', 'domain_mode': 'pic_ngo'}, []) + self.assertIn('auto_https off', out) + self.assertNotIn('dns pic_ngo', out) + + class TestDdnsApiStripsLegacySuffix(unittest.TestCase): """_caddyfile_pic_ngo strips /api/v1 from ddns_api so the plugin doesn't double it."""