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."""