mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
dyndns: various tweaks to simplify the code, improve UX ...
This commit is contained in:
parent
c98da124b2
commit
e2da51b9a3
5 changed files with 62 additions and 99 deletions
|
@ -90,7 +90,8 @@
|
|||
"ask_new_domain": "New domain",
|
||||
"ask_new_path": "New path",
|
||||
"ask_password": "Password",
|
||||
"ask_dyndns_recovery_password": "DynDNS recovey 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": "DynDNS recovey passwory",
|
||||
"ask_user_domain": "Domain to use for the user's email address and XMPP account",
|
||||
"backup_abstract_method": "This backup method has yet to be implemented",
|
||||
"backup_actually_backuping": "Creating a backup archive from the collected files...",
|
||||
|
@ -383,8 +384,6 @@
|
|||
"domain_dns_registrar_supported": "YunoHost automatically detected that this domain is handled by the registrar **{registrar}**. If you want, YunoHost will automatically configure this DNS zone, if you provide it with the appropriate API credentials. You can find documentation on how to obtain your API credentials on this page: https://yunohost.org/registar_api_{registrar}. (You can also manually configure your DNS records following the documentation at https://yunohost.org/dns )",
|
||||
"domain_dns_registrar_yunohost": "This domain is a nohost.me / nohost.st / ynh.fr and its DNS configuration is therefore automatically handled by YunoHost without any further configuration. (see the 'yunohost dyndns update' command)",
|
||||
"domain_dyndns_already_subscribed": "You have already subscribed to a DynDNS domain",
|
||||
"domain_dyndns_instruction_unclear": "You must choose exactly one of the following options : --subscribe or --ignore-dyndns",
|
||||
"domain_dyndns_instruction_unclear_unsubscribe": "You must choose exactly one of the following options : --unsubscribe or --ignore-dyndns",
|
||||
"domain_exists": "The domain already exists",
|
||||
"domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).",
|
||||
"domain_registrar_is_not_configured": "The registrar is not yet configured for domain {domain}.",
|
||||
|
@ -400,7 +399,6 @@
|
|||
"dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.",
|
||||
"dyndns_ip_update_failed": "Could not update IP address to DynDNS",
|
||||
"dyndns_ip_updated": "Updated your IP on DynDNS",
|
||||
"dyndns_key_generating": "Generating DNS key... It may take a while.",
|
||||
"dyndns_key_not_found": "DNS key not found for the domain",
|
||||
"dyndns_no_domain_registered": "No domain registered with DynDNS",
|
||||
"dyndns_no_recovery_password": "No recovery password specified! In case you loose control of this domain, you will need to contact an administrator in the YunoHost team!",
|
||||
|
@ -772,4 +770,4 @@
|
|||
"yunohost_installing": "Installing YunoHost...",
|
||||
"yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'",
|
||||
"yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -493,7 +493,7 @@ domain:
|
|||
help: Display domains as a tree
|
||||
action: store_true
|
||||
--features:
|
||||
help: List only domains with features enabled (xmpp, mail_in, mail_out, auto_push)
|
||||
help: List only domains with features enabled (xmpp, mail_in, mail_out)
|
||||
nargs: "*"
|
||||
|
||||
### domain_info()
|
||||
|
|
|
@ -228,7 +228,7 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d
|
|||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
from yunohost.certificate import _certificate_install_selfsigned
|
||||
|
||||
if dyndns_recovery_password != 0 and dyndns_recovery_password is not None:
|
||||
if dyndns_recovery_password:
|
||||
operation_logger.data_to_redact.append(dyndns_recovery_password)
|
||||
|
||||
if domain.startswith("xmpp-upload."):
|
||||
|
@ -252,35 +252,19 @@ def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_d
|
|||
domain = domain.encode("idna").decode("utf-8")
|
||||
|
||||
# Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain )
|
||||
dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3
|
||||
dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3
|
||||
if dyndns:
|
||||
if not ignore_dyndns and not dyndns_recovery_password:
|
||||
if Moulinette.interface.type == "api":
|
||||
raise YunohostValidationError("domain_dyndns_missing_password")
|
||||
else:
|
||||
dyndns_recovery_password = Moulinette.prompt(
|
||||
m18n.n("ask_dyndns_recovery_password"), is_password=True, confirm=True
|
||||
)
|
||||
|
||||
# Ensure sufficiently complex password
|
||||
assert_password_is_strong_enough("admin", dyndns_recovery_password)
|
||||
|
||||
if ((dyndns_recovery_password is None) == (ignore_dyndns is False)):
|
||||
raise YunohostValidationError("domain_dyndns_instruction_unclear")
|
||||
|
||||
from yunohost.dyndns import is_subscribing_allowed
|
||||
|
||||
# Do not allow to subscribe to multiple dyndns domains...
|
||||
if not is_subscribing_allowed():
|
||||
raise YunohostValidationError("domain_dyndns_already_subscribed")
|
||||
if dyndns_recovery_password:
|
||||
assert_password_is_strong_enough("admin", dyndns_recovery_password)
|
||||
|
||||
operation_logger.start()
|
||||
if not dyndns and (dyndns_recovery_password is not None or ignore_dyndns):
|
||||
logger.warning("This domain is not a DynDNS one, no need for the --dyndns-recovery-password or --ignore-dyndns option")
|
||||
|
||||
if dyndns and not ignore_dyndns:
|
||||
# Actually subscribe
|
||||
domain_dyndns_subscribe(domain=domain, password=dyndns_recovery_password)
|
||||
if dyndns:
|
||||
domain_dyndns_subscribe(domain=domain, recovery_password=dyndns_recovery_password)
|
||||
|
||||
_certificate_install_selfsigned([domain], True)
|
||||
|
||||
|
@ -346,7 +330,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd
|
|||
from yunohost.app import app_ssowatconf, app_info, app_remove
|
||||
from yunohost.utils.ldap import _get_ldap_interface
|
||||
|
||||
if dyndns_recovery_password != 0 and dyndns_recovery_password is not None:
|
||||
if dyndns_recovery_password:
|
||||
operation_logger.data_to_redact.append(dyndns_recovery_password)
|
||||
|
||||
# the 'force' here is related to the exception happening in domain_add ...
|
||||
|
@ -410,16 +394,10 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd
|
|||
)
|
||||
|
||||
# Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain )
|
||||
dyndns = is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3
|
||||
if dyndns:
|
||||
if ((dyndns_recovery_password is None) == (ignore_dyndns is False)):
|
||||
raise YunohostValidationError("domain_dyndns_instruction_unclear_unsubscribe")
|
||||
dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
if not dyndns and ((dyndns_recovery_password is not None) or (ignore_dyndns is not False)):
|
||||
logger.warning("This domain is not a DynDNS one, no need for the --dyndns_recovery_password or --ignore-dyndns option")
|
||||
|
||||
ldap = _get_ldap_interface()
|
||||
try:
|
||||
ldap.remove("virtualdomain=" + domain + ",ou=domains")
|
||||
|
@ -465,9 +443,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd
|
|||
hook_callback("post_domain_remove", args=[domain])
|
||||
|
||||
# If a password is provided, delete the DynDNS record
|
||||
if dyndns and not ignore_dyndns:
|
||||
if dyndns:
|
||||
# Actually unsubscribe
|
||||
domain_dyndns_unsubscribe(domain=domain, password=dyndns_recovery_password)
|
||||
domain_dyndns_unsubscribe(domain=domain, recovery_password=dyndns_recovery_password)
|
||||
|
||||
logger.success(m18n.n("domain_deleted"))
|
||||
|
||||
|
|
102
src/dyndns.py
102
src/dyndns.py
|
@ -50,7 +50,7 @@ def is_subscribing_allowed():
|
|||
Returns:
|
||||
True if the limit is not reached, False otherwise
|
||||
"""
|
||||
return len(glob.glob("/etc/yunohost/dyndns/*.key")) < MAX_DYNDNS_DOMAINS
|
||||
return len(dyndns_list()["domains"]) < MAX_DYNDNS_DOMAINS
|
||||
|
||||
|
||||
def _dyndns_available(domain):
|
||||
|
@ -78,7 +78,7 @@ def _dyndns_available(domain):
|
|||
return r == f"Domain {domain} is available"
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
@is_unit_operation(exclude=["recovery_password"])
|
||||
def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
||||
"""
|
||||
Subscribe to a DynDNS service
|
||||
|
@ -88,26 +88,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
|||
recovery_password -- Password that will be used to delete the domain
|
||||
"""
|
||||
|
||||
if recovery_password is None:
|
||||
logger.warning(m18n.n('dyndns_no_recovery_password'))
|
||||
else:
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
# Ensure sufficiently complex password
|
||||
if Moulinette.interface.type == "cli" and recovery_password == 0:
|
||||
recovery_password = Moulinette.prompt(
|
||||
m18n.n("ask_password"),
|
||||
is_password=True,
|
||||
confirm=True
|
||||
)
|
||||
assert_password_is_strong_enough("admin", recovery_password)
|
||||
|
||||
if not is_subscribing_allowed():
|
||||
raise YunohostValidationError("domain_dyndns_already_subscribed")
|
||||
|
||||
if domain is None:
|
||||
domain = _get_maindomain()
|
||||
operation_logger.related_to.append(("domain", domain))
|
||||
|
||||
# Verify if domain is provided by subscribe_host
|
||||
if not is_yunohost_dyndns_domain(domain):
|
||||
raise YunohostValidationError(
|
||||
|
@ -118,6 +98,30 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
|||
if not _dyndns_available(domain):
|
||||
raise YunohostValidationError("dyndns_unavailable", domain=domain)
|
||||
|
||||
# Check adding another dyndns domain is still allowed
|
||||
if not is_subscribing_allowed():
|
||||
raise YunohostValidationError("domain_dyndns_already_subscribed")
|
||||
|
||||
# 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"))
|
||||
recovery_password = Moulinette.prompt(
|
||||
m18n.n("ask_dyndns_recovery_password"),
|
||||
is_password=True,
|
||||
confirm=True
|
||||
)
|
||||
elif not recovery_password:
|
||||
logger.warning(m18n.n("dyndns_no_recovery_password"))
|
||||
|
||||
if recovery_password:
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
assert_password_is_strong_enough("admin", recovery_password)
|
||||
operation_logger.data_to_redact.append(recovery_password)
|
||||
|
||||
if domain is None:
|
||||
domain = _get_maindomain()
|
||||
operation_logger.related_to.append(("domain", domain))
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
# '165' is the convention identifier for hmac-sha512 algorithm
|
||||
|
@ -127,8 +131,6 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
|||
if not os.path.exists("/etc/yunohost/dyndns"):
|
||||
os.makedirs("/etc/yunohost/dyndns")
|
||||
|
||||
logger.debug(m18n.n("dyndns_key_generating"))
|
||||
|
||||
# Here, we emulate the behavior of the old 'dnssec-keygen' utility
|
||||
# which since bullseye was replaced by ddns-keygen which is now
|
||||
# in the bind9 package ... but installing bind9 will conflict with dnsmasq
|
||||
|
@ -154,7 +156,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
|||
# Yeah the secret is already a base64-encoded but we double-bas64-encode it, whatever...
|
||||
b64encoded_key = base64.b64encode(secret.encode()).decode()
|
||||
data = {"subdomain": domain}
|
||||
if password is not None:
|
||||
if recovery_password:
|
||||
data["recovery_password"] = hashlib.sha256((domain + ":" + recovery_password.strip()).encode('utf-8')).hexdigest()
|
||||
r = requests.post(
|
||||
f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512",
|
||||
|
@ -188,7 +190,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
|||
logger.success(m18n.n("dyndns_registered"))
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
@is_unit_operation(exclude=["recovery_password"])
|
||||
def dyndns_unsubscribe(operation_logger, domain, recovery_password=None):
|
||||
"""
|
||||
Unsubscribe from a DynDNS service
|
||||
|
@ -198,24 +200,19 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None):
|
|||
recovery_password -- Password that is used to delete the domain ( defined when subscribing )
|
||||
"""
|
||||
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
import requests # lazy loading this module for performance reasons
|
||||
|
||||
# FIXME : it should be possible to unsubscribe the domain just using the key file ...
|
||||
|
||||
# Ensure sufficiently complex password
|
||||
if Moulinette.interface.type == "cli" and not recovery_password:
|
||||
recovery_password = Moulinette.prompt(
|
||||
m18n.n("ask_password"),
|
||||
m18n.n("ask_dyndns_recovery_password"),
|
||||
is_password=True
|
||||
)
|
||||
assert_password_is_strong_enough("admin", recovery_password)
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
# '165' is the convention identifier for hmac-sha512 algorithm
|
||||
# '1234' is idk? doesnt matter, but the old format contained a number here...
|
||||
key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key"
|
||||
|
||||
import requests # lazy loading this module for performance reasons
|
||||
|
||||
# Send delete request
|
||||
try:
|
||||
secret = str(domain) + ":" + str(recovery_password).strip()
|
||||
|
@ -228,30 +225,30 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None):
|
|||
raise YunohostError("dyndns_unregistration_failed", error=str(e))
|
||||
|
||||
if r.status_code == 200: # Deletion was successful
|
||||
rm(key_file, force=True)
|
||||
for key_file in glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key"):
|
||||
rm(key_file, force=True)
|
||||
# Yunohost regen conf will add the dyndns cron job if a key exists
|
||||
# in /etc/yunohost/dyndns
|
||||
regen_conf(["yunohost"])
|
||||
|
||||
logger.success(m18n.n("dyndns_unregistered"))
|
||||
elif r.status_code == 403: # Wrong password
|
||||
raise YunohostError("dyndns_unsubscribe_wrong_password")
|
||||
elif r.status_code == 404: # Invalid domain
|
||||
raise YunohostError("dyndns_unsubscribe_wrong_domain")
|
||||
|
||||
logger.success(m18n.n("dyndns_unregistered"))
|
||||
|
||||
|
||||
def dyndns_list():
|
||||
"""
|
||||
Returns all currently subscribed DynDNS domains ( deduced from the key files )
|
||||
"""
|
||||
|
||||
files = glob.glob("/etc/yunohost/dyndns/K*key")
|
||||
# Get the domain names
|
||||
for i in range(len(files)):
|
||||
files[i] = files[i].split(".+", 1)[0]
|
||||
files[i] = files[i].split("/etc/yunohost/dyndns/K")[1]
|
||||
from yunohost.domain import domain_list
|
||||
|
||||
return {"domains": files}
|
||||
domains = domain_list(exclude_subdomains=True)["domains"]
|
||||
dyndns_domains = [d for d in domains if is_yunohost_dyndns_domain(d) and glob.glob(f"/etc/yunohost/dyndns/K{d}.+*.key")]
|
||||
|
||||
return {"domains": dyndns_domains}
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
|
@ -277,21 +274,14 @@ def dyndns_update(
|
|||
# If domain is not given, update all DynDNS domains
|
||||
if domain is None:
|
||||
|
||||
from yunohost.domain import domain_list
|
||||
dyndns_domains = dyndns_list()["domains"]
|
||||
|
||||
domains = domain_list(exclude_subdomains=True, auto_push=True)["domains"]
|
||||
pushed = 0
|
||||
for d in domains:
|
||||
if is_yunohost_dyndns_domain(d):
|
||||
dyndns_update(d, force=force, dry_run=dry_run)
|
||||
pushed += 1
|
||||
if pushed == 0:
|
||||
if not dyndns_domains:
|
||||
raise YunohostValidationError("dyndns_no_domain_registered")
|
||||
return
|
||||
|
||||
elif type(domain).__name__ in ["list", "tuple"]:
|
||||
for d in domain:
|
||||
dyndns_update(d, force=force, dry_run=dry_run)
|
||||
for domain in dyndns_domains:
|
||||
dyndns_update(domain, force=force, dry_run=dry_run)
|
||||
|
||||
return
|
||||
|
||||
# If key is not given, pick the first file we find with the domain given
|
||||
|
|
|
@ -197,11 +197,8 @@ def tools_postinstall(
|
|||
assert_password_is_strong_enough("admin", password)
|
||||
|
||||
# If this is a nohost.me/noho.st, actually check for availability
|
||||
if not ignore_dyndns and is_yunohost_dyndns_domain(domain):
|
||||
|
||||
if (bool(dyndns_recovery_password), ignore_dyndns) in [(True, True), (False, False)]:
|
||||
raise YunohostValidationError("domain_dyndns_instruction_unclear")
|
||||
|
||||
dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain)
|
||||
if dyndns:
|
||||
# Check if the domain is available...
|
||||
try:
|
||||
available = _dyndns_available(domain)
|
||||
|
|
Loading…
Add table
Reference in a new issue