mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #1715 from YunoHost/dyndns
enh: Handle dyndns subscribe/unsubscribe in web admin
This commit is contained in:
commit
c6633873ba
6 changed files with 109 additions and 37 deletions
|
@ -91,6 +91,7 @@
|
||||||
"ask_new_path": "New path",
|
"ask_new_path": "New path",
|
||||||
"ask_password": "Password",
|
"ask_password": "Password",
|
||||||
"ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.",
|
"ask_dyndns_recovery_password_explain": "Please pick a recovery password for your DynDNS domain, in case you need to reset it later.",
|
||||||
|
"ask_dyndns_recovery_password_explain_unavailable": "This DynDNS domain is already registered. If you are the person who originally registered this domain, you may enter the recovery password to reclaim this domain.",
|
||||||
"ask_dyndns_recovery_password": "DynDNS recovery password",
|
"ask_dyndns_recovery_password": "DynDNS recovery password",
|
||||||
"ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.",
|
"ask_dyndns_recovery_password_explain_during_unsubscribe": "Please enter the recovery password for this DynDNS domain.",
|
||||||
"ask_user_domain": "Domain to use for the user's email address and XMPP account",
|
"ask_user_domain": "Domain to use for the user's email address and XMPP account",
|
||||||
|
@ -406,6 +407,7 @@
|
||||||
"dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.",
|
"dyndns_provider_unreachable": "Unable to reach DynDNS provider {provider}: either your YunoHost is not correctly connected to the internet or the dynette server is down.",
|
||||||
"dyndns_subscribed": "DynDNS domain subscribed",
|
"dyndns_subscribed": "DynDNS domain subscribed",
|
||||||
"dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}",
|
"dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}",
|
||||||
|
"dyndns_too_many_requests": "YunoHost's dyndns service received too many requests from you, wait 1 hour or so before trying again.",
|
||||||
"dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}",
|
"dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}",
|
||||||
"dyndns_unsubscribed": "DynDNS domain unsubscribed",
|
"dyndns_unsubscribed": "DynDNS domain unsubscribed",
|
||||||
"dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials",
|
"dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials",
|
||||||
|
|
|
@ -545,6 +545,9 @@ def _get_registrar_config_section(domain):
|
||||||
"value": "yunohost",
|
"value": "yunohost",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
registrar_infos["recovery_password"] = OrderedDict(
|
||||||
|
{"type": "password", "ask": m18n.n("ask_dyndns_recovery_password"), "default": ""}
|
||||||
|
)
|
||||||
return OrderedDict(registrar_infos)
|
return OrderedDict(registrar_infos)
|
||||||
elif is_special_use_tld(dns_zone):
|
elif is_special_use_tld(dns_zone):
|
||||||
registrar_infos["registrar"] = OrderedDict(
|
registrar_infos["registrar"] = OrderedDict(
|
||||||
|
|
|
@ -342,6 +342,7 @@ def domain_remove(
|
||||||
dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain
|
dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain
|
||||||
ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing
|
ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing
|
||||||
"""
|
"""
|
||||||
|
import glob
|
||||||
from yunohost.hook import hook_callback
|
from yunohost.hook import hook_callback
|
||||||
from yunohost.app import app_ssowatconf, app_info, app_remove
|
from yunohost.app import app_ssowatconf, app_info, app_remove
|
||||||
from yunohost.utils.ldap import _get_ldap_interface
|
from yunohost.utils.ldap import _get_ldap_interface
|
||||||
|
@ -427,14 +428,20 @@ def domain_remove(
|
||||||
global domain_list_cache
|
global domain_list_cache
|
||||||
domain_list_cache = []
|
domain_list_cache = []
|
||||||
|
|
||||||
stuff_to_delete = [
|
# If a password is provided, delete the DynDNS record
|
||||||
f"/etc/yunohost/certs/{domain}",
|
if dyndns:
|
||||||
f"/etc/yunohost/dyndns/K{domain}.+*",
|
try:
|
||||||
f"{DOMAIN_SETTINGS_DIR}/{domain}.yml",
|
# Actually unsubscribe
|
||||||
]
|
domain_dyndns_unsubscribe(
|
||||||
|
domain=domain, recovery_password=dyndns_recovery_password
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(str(e))
|
||||||
|
|
||||||
for stuff in stuff_to_delete:
|
rm(f"/etc/yunohost/certs/{domain}", force=True, recursive=True)
|
||||||
rm(stuff, force=True, recursive=True)
|
for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*"):
|
||||||
|
rm(key_file, force=True)
|
||||||
|
rm(f"{DOMAIN_SETTINGS_DIR}/{domain}.yml", force=True)
|
||||||
|
|
||||||
# Sometime we have weird issues with the regenconf where some files
|
# Sometime we have weird issues with the regenconf where some files
|
||||||
# appears as manually modified even though they weren't touched ...
|
# appears as manually modified even though they weren't touched ...
|
||||||
|
@ -462,13 +469,6 @@ def domain_remove(
|
||||||
|
|
||||||
hook_callback("post_domain_remove", args=[domain])
|
hook_callback("post_domain_remove", args=[domain])
|
||||||
|
|
||||||
# If a password is provided, delete the DynDNS record
|
|
||||||
if dyndns:
|
|
||||||
# Actually unsubscribe
|
|
||||||
domain_dyndns_unsubscribe(
|
|
||||||
domain=domain, recovery_password=dyndns_recovery_password
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.success(m18n.n("domain_deleted"))
|
logger.success(m18n.n("domain_deleted"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -707,6 +707,19 @@ class DomainConfigPanel(ConfigPanel):
|
||||||
domain=self.entity,
|
domain=self.entity,
|
||||||
other_app=app_map(raw=True)[self.entity]["/"]["id"],
|
other_app=app_map(raw=True)[self.entity]["/"]["id"],
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
"recovery_password" in self.new_values
|
||||||
|
and self.new_values["recovery_password"]
|
||||||
|
):
|
||||||
|
domain_dyndns_set_recovery_password(
|
||||||
|
self.entity, self.new_values["recovery_password"]
|
||||||
|
)
|
||||||
|
# Do not save password in yaml settings
|
||||||
|
if "recovery_password" in self.values:
|
||||||
|
del self.values["recovery_password"]
|
||||||
|
if "recovery_password" in self.new_values:
|
||||||
|
del self.new_values["recovery_password"]
|
||||||
|
assert "recovery_password" not in self.future_values
|
||||||
|
|
||||||
super()._apply()
|
super()._apply()
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ from moulinette import Moulinette, m18n
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import MoulinetteError
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
from moulinette.utils.filesystem import write_to_file, rm, chown, chmod
|
from moulinette.utils.filesystem import write_to_file, rm, chown, chmod
|
||||||
from moulinette.utils.network import download_json
|
|
||||||
|
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
from yunohost.domain import _get_maindomain
|
from yunohost.domain import _get_maindomain
|
||||||
|
@ -63,19 +62,28 @@ def _dyndns_available(domain):
|
||||||
Returns:
|
Returns:
|
||||||
True if the domain is available, False otherwise.
|
True if the domain is available, False otherwise.
|
||||||
"""
|
"""
|
||||||
|
import requests # lazy loading this module for performance reasons
|
||||||
|
|
||||||
logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...")
|
logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = download_json(
|
r = requests.get(f"https://{DYNDNS_PROVIDER}/test/{domain}", timeout=30)
|
||||||
f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None
|
|
||||||
)
|
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
raise YunohostError(
|
raise YunohostError(
|
||||||
"dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER
|
"dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER
|
||||||
)
|
)
|
||||||
|
|
||||||
return r == f"Domain {domain} is available"
|
if r.status_code == 200:
|
||||||
|
return r.text.strip('"') == f"Domain {domain} is available"
|
||||||
|
elif r.status_code == 409:
|
||||||
|
return False
|
||||||
|
elif r.status_code == 429:
|
||||||
|
raise YunohostValidationError("dyndns_too_many_requests")
|
||||||
|
else:
|
||||||
|
raise YunohostError(
|
||||||
|
"dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation(exclude=["recovery_password"])
|
@is_unit_operation(exclude=["recovery_password"])
|
||||||
|
@ -94,14 +102,26 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
||||||
"dyndns_domain_not_provided", domain=domain, provider=DYNDNS_PROVIDER
|
"dyndns_domain_not_provided", domain=domain, provider=DYNDNS_PROVIDER
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify if domain is available
|
|
||||||
if not _dyndns_available(domain):
|
|
||||||
raise YunohostValidationError("dyndns_unavailable", domain=domain)
|
|
||||||
|
|
||||||
# Check adding another dyndns domain is still allowed
|
# Check adding another dyndns domain is still allowed
|
||||||
if not is_subscribing_allowed():
|
if not is_subscribing_allowed():
|
||||||
raise YunohostValidationError("domain_dyndns_already_subscribed")
|
raise YunohostValidationError("domain_dyndns_already_subscribed")
|
||||||
|
|
||||||
|
# Verify if domain is available
|
||||||
|
if not _dyndns_available(domain):
|
||||||
|
# Prompt for a password if running in CLI and no password provided
|
||||||
|
if not recovery_password and Moulinette.interface.type == "cli":
|
||||||
|
logger.warning(m18n.n("ask_dyndns_recovery_password_explain_unavailable"))
|
||||||
|
recovery_password = Moulinette.prompt(
|
||||||
|
m18n.n("ask_dyndns_recovery_password"), is_password=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if recovery_password:
|
||||||
|
# Try to unsubscribe the domain so it can be subscribed again
|
||||||
|
# If successful, it will be resubscribed with the same recovery password
|
||||||
|
dyndns_unsubscribe(domain=domain, recovery_password=recovery_password)
|
||||||
|
else:
|
||||||
|
raise YunohostValidationError("dyndns_unavailable", domain=domain)
|
||||||
|
|
||||||
# Prompt for a password if running in CLI and no password provided
|
# Prompt for a password if running in CLI and no password provided
|
||||||
if not recovery_password and Moulinette.interface.type == "cli":
|
if not recovery_password and Moulinette.interface.type == "cli":
|
||||||
logger.warning(m18n.n("ask_dyndns_recovery_password_explain"))
|
logger.warning(m18n.n("ask_dyndns_recovery_password_explain"))
|
||||||
|
@ -252,9 +272,11 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None):
|
||||||
# in /etc/yunohost/dyndns
|
# in /etc/yunohost/dyndns
|
||||||
regen_conf(["yunohost"])
|
regen_conf(["yunohost"])
|
||||||
elif r.status_code == 403:
|
elif r.status_code == 403:
|
||||||
raise YunohostError("dyndns_unsubscribe_denied")
|
raise YunohostValidationError("dyndns_unsubscribe_denied")
|
||||||
elif r.status_code == 409:
|
elif r.status_code == 409:
|
||||||
raise YunohostError("dyndns_unsubscribe_already_unsubscribed")
|
raise YunohostValidationError("dyndns_unsubscribe_already_unsubscribed")
|
||||||
|
elif r.status_code == 429:
|
||||||
|
raise YunohostValidationError("dyndns_too_many_requests")
|
||||||
else:
|
else:
|
||||||
raise YunohostError(
|
raise YunohostError(
|
||||||
"dyndns_unsubscribe_failed",
|
"dyndns_unsubscribe_failed",
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from moulinette import Moulinette
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||||
|
@ -75,11 +77,41 @@ def test_domain_add():
|
||||||
assert TEST_DOMAINS[2] in domain_list()["domains"]
|
assert TEST_DOMAINS[2] in domain_list()["domains"]
|
||||||
|
|
||||||
|
|
||||||
def test_domain_add_subscribe():
|
def test_domain_add_and_remove_dyndns():
|
||||||
time.sleep(35) # Dynette blocks requests that happen too frequently
|
# Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt
|
||||||
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||||
domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
||||||
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"]
|
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"]
|
||||||
|
domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
||||||
|
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_domain_dyndns_recovery():
|
||||||
|
# Devs: if you get `too_many_request` errors, ask the team to add your IP to the rate limit excempt
|
||||||
|
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||||
|
# mocked as API call to avoid CLI prompts
|
||||||
|
with patch.object(Moulinette.interface, "type", "api"):
|
||||||
|
# add domain without recovery password
|
||||||
|
domain_add(TEST_DYNDNS_DOMAIN)
|
||||||
|
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"]
|
||||||
|
# set the recovery password with config panel
|
||||||
|
domain_config_set(TEST_DYNDNS_DOMAIN, "dns.registrar.recovery_password", TEST_DYNDNS_PASSWORD)
|
||||||
|
# remove domain without unsubscribing
|
||||||
|
domain_remove(TEST_DYNDNS_DOMAIN, ignore_dyndns=True)
|
||||||
|
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||||
|
# readding domain with bad password should fail
|
||||||
|
with pytest.raises(YunohostValidationError):
|
||||||
|
domain_add(
|
||||||
|
TEST_DYNDNS_DOMAIN, dyndns_recovery_password="wrong" + TEST_DYNDNS_PASSWORD
|
||||||
|
)
|
||||||
|
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||||
|
# readding domain with password should work
|
||||||
|
domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
||||||
|
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"]
|
||||||
|
# remove the dyndns domain
|
||||||
|
domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
||||||
|
|
||||||
|
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||||
|
|
||||||
|
|
||||||
def test_domain_add_existing_domain():
|
def test_domain_add_existing_domain():
|
||||||
|
@ -94,13 +126,6 @@ def test_domain_remove():
|
||||||
assert TEST_DOMAINS[1] not in domain_list()["domains"]
|
assert TEST_DOMAINS[1] not in domain_list()["domains"]
|
||||||
|
|
||||||
|
|
||||||
def test_domain_remove_unsubscribe():
|
|
||||||
time.sleep(35) # Dynette blocks requests that happen too frequently
|
|
||||||
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"]
|
|
||||||
domain_remove(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
|
||||||
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_main_domain():
|
def test_main_domain():
|
||||||
current_main_domain = _get_maindomain()
|
current_main_domain = _get_maindomain()
|
||||||
assert domain_main_domain()["current_main_domain"] == current_main_domain
|
assert domain_main_domain()["current_main_domain"] == current_main_domain
|
||||||
|
|
11
src/tools.py
11
src/tools.py
|
@ -156,7 +156,7 @@ def tools_postinstall(
|
||||||
force_diskspace=False,
|
force_diskspace=False,
|
||||||
overwrite_root_password=True,
|
overwrite_root_password=True,
|
||||||
):
|
):
|
||||||
from yunohost.dyndns import _dyndns_available
|
from yunohost.dyndns import _dyndns_available, dyndns_unsubscribe
|
||||||
from yunohost.utils.dns import is_yunohost_dyndns_domain
|
from yunohost.utils.dns import is_yunohost_dyndns_domain
|
||||||
from yunohost.utils.password import (
|
from yunohost.utils.password import (
|
||||||
assert_password_is_strong_enough,
|
assert_password_is_strong_enough,
|
||||||
|
@ -218,7 +218,14 @@ def tools_postinstall(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not available:
|
if not available:
|
||||||
raise YunohostValidationError("dyndns_unavailable", domain=domain)
|
if dyndns_recovery_password:
|
||||||
|
# Try to unsubscribe the domain so it can be subscribed again
|
||||||
|
# If successful, it will be resubscribed with the same recovery password
|
||||||
|
dyndns_unsubscribe(
|
||||||
|
domain=domain, recovery_password=dyndns_recovery_password
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise YunohostValidationError("dyndns_unavailable", domain=domain)
|
||||||
|
|
||||||
if os.system("iptables -V >/dev/null 2>/dev/null") != 0:
|
if os.system("iptables -V >/dev/null 2>/dev/null") != 0:
|
||||||
raise YunohostValidationError(
|
raise YunohostValidationError(
|
||||||
|
|
Loading…
Add table
Reference in a new issue