Fix DDNS security and reliability gaps (#2, #3, #5, #6, #7)
Unit Tests / test (push) Successful in 7m23s
Unit Tests / test (push) Successful in 7m23s
- Fix #2: Move DDNS bearer token from cell_config.json to data/api/ddns_token. Token is now in the secrets store (data/) rather than the config store (config/). Auto-migrates existing installs on first access. ConfigManager.get/set_ddns_token() added. set_ddns_config() now strips 'token' key to prevent it leaking back. - Fix #3: Set Caddyfile permissions to 0o600 after write so the token embedded in the Caddyfile is not world-readable on the host filesystem. - Fix #5: Heartbeat now fires IDENTITY_CHANGED after re-registration so Caddy regenerates its config with the new token automatically — users no longer need to click Re-register in Settings after a wizard registration failure. Also: heartbeat skips the 401-cycle when no token exists and goes straight to registration instead. DDNSManager now accepts service_bus= and is wired up. - Fix #6: Settings page starts polling GET /api/caddy/cert-status every 15s after a successful DDNS re-registration and shows "Acquiring certificate…" feedback until Let's Encrypt issues the cert (up to 5 minutes). - Fix #7: regenerate_with_installed() is debounced (5 s window) so two rapid IDENTITY_CHANGED events (e.g. wizard + heartbeat) can't start simultaneous ACME orders that interfere with each other. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,8 +60,9 @@ class TestGenerateCaddyfilePicNgo(unittest.TestCase):
|
||||
def test_pic_ngo_has_dns_plugin_and_wildcard(self):
|
||||
mgr = _mgr()
|
||||
mgr.config_manager.configs = {
|
||||
'ddns': {'token': 'TESTSECRET123', 'url': 'https://ddns.pic.ngo/api/v1'},
|
||||
'ddns': {'url': 'https://ddns.pic.ngo/api/v1'},
|
||||
}
|
||||
mgr.config_manager.get_ddns_token.return_value = 'TESTSECRET123'
|
||||
identity = {'cell_name': 'alpha', 'domain_mode': 'pic_ngo'}
|
||||
with unittest.mock.patch.dict(os.environ, {'DDNS_URL': 'https://ddns.pic.ngo/api/v1'}):
|
||||
out = mgr.generate_caddyfile(identity, [])
|
||||
@@ -81,7 +82,8 @@ class TestGenerateCaddyfilePicNgo(unittest.TestCase):
|
||||
|
||||
def test_pic_ngo_acme_ca_included_when_env_set(self):
|
||||
mgr = _mgr()
|
||||
mgr.config_manager.configs = {'ddns': {'token': 'TESTSECRET123'}}
|
||||
mgr.config_manager.configs = {'ddns': {}}
|
||||
mgr.config_manager.get_ddns_token.return_value = 'TESTSECRET123'
|
||||
identity = {'cell_name': 'alpha', 'domain_mode': 'pic_ngo'}
|
||||
with unittest.mock.patch.dict(os.environ, {
|
||||
'DDNS_URL': 'https://ddns.pic.ngo/api/v1',
|
||||
@@ -645,7 +647,8 @@ class TestPicNgoNoTokenFallback(unittest.TestCase):
|
||||
|
||||
def test_empty_token_generates_lan_caddyfile(self):
|
||||
mgr = _mgr()
|
||||
mgr.config_manager.configs = {'ddns': {'token': '', 'url': 'https://ddns.pic.ngo'}}
|
||||
mgr.config_manager.configs = {'ddns': {'url': 'https://ddns.pic.ngo'}}
|
||||
mgr.config_manager.get_ddns_token.return_value = ''
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop('DDNS_TOKEN', None)
|
||||
os.environ.pop('DDNS_URL', None)
|
||||
@@ -657,6 +660,7 @@ class TestPicNgoNoTokenFallback(unittest.TestCase):
|
||||
def test_missing_ddns_config_generates_lan_caddyfile(self):
|
||||
mgr = _mgr()
|
||||
mgr.config_manager.configs = {}
|
||||
mgr.config_manager.get_ddns_token.return_value = ''
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop('DDNS_TOKEN', None)
|
||||
os.environ.pop('DDNS_URL', None)
|
||||
@@ -671,8 +675,9 @@ class TestDdnsApiStripsLegacySuffix(unittest.TestCase):
|
||||
def test_api_v1_suffix_stripped_from_config_url(self):
|
||||
mgr = _mgr()
|
||||
mgr.config_manager.configs = {
|
||||
'ddns': {'token': 'tok', 'url': 'https://ddns.pic.ngo/api/v1'},
|
||||
'ddns': {'url': 'https://ddns.pic.ngo/api/v1'},
|
||||
}
|
||||
mgr.config_manager.get_ddns_token.return_value = 'tok'
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop('DDNS_URL', None)
|
||||
out = mgr.generate_caddyfile({'cell_name': 'x', 'domain_mode': 'pic_ngo'}, [])
|
||||
@@ -682,8 +687,9 @@ class TestDdnsApiStripsLegacySuffix(unittest.TestCase):
|
||||
def test_clean_url_is_unchanged(self):
|
||||
mgr = _mgr()
|
||||
mgr.config_manager.configs = {
|
||||
'ddns': {'token': 'tok', 'url': 'https://ddns.pic.ngo'},
|
||||
'ddns': {'url': 'https://ddns.pic.ngo'},
|
||||
}
|
||||
mgr.config_manager.get_ddns_token.return_value = 'tok'
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop('DDNS_URL', None)
|
||||
out = mgr.generate_caddyfile({'cell_name': 'x', 'domain_mode': 'pic_ngo'}, [])
|
||||
|
||||
Reference in New Issue
Block a user