mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #1475 from YunoHost/dyndns-password
Dyndns recovery password
This commit is contained in:
commit
7e18e8c9ec
7 changed files with 427 additions and 134 deletions
|
@ -90,6 +90,9 @@
|
|||
"ask_new_domain": "New domain",
|
||||
"ask_new_path": "New path",
|
||||
"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": "DynDNS recovery password",
|
||||
"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",
|
||||
"backup_abstract_method": "This backup method has yet to be implemented",
|
||||
"backup_actually_backuping": "Creating a backup archive from the collected files...",
|
||||
|
@ -382,7 +385,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_root_unknown": "Unknown DynDNS root domain",
|
||||
"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}.",
|
||||
|
@ -398,12 +400,21 @@
|
|||
"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!",
|
||||
"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_registered": "DynDNS domain registered",
|
||||
"dyndns_registration_failed": "Could not register DynDNS domain: {error}",
|
||||
"dyndns_subscribed": "DynDNS domain subscribed",
|
||||
"dyndns_subscribe_failed": "Could not subscribe DynDNS domain: {error}",
|
||||
"dyndns_unsubscribe_failed": "Could not unsubscribe DynDNS domain: {error}",
|
||||
"dyndns_unsubscribed": "DynDNS domain unsubscribed",
|
||||
"dyndns_unsubscribe_denied": "Failed to unsubscribe domain: invalid credentials",
|
||||
"dyndns_unsubscribe_already_unsubscribed": "Domain is already unsubscribed",
|
||||
"dyndns_set_recovery_password_denied": "Failed to set recovery password: invalid key",
|
||||
"dyndns_set_recovery_password_unknown_domain": "Failed to set recovery password: domain not registered",
|
||||
"dyndns_set_recovery_password_invalid_password": "Failed to set recovery password: password is not strong enough",
|
||||
"dyndns_set_recovery_password_failed": "Failed to set recovery password: {error}",
|
||||
"dyndns_set_recovery_password_success": "Recovery password set!",
|
||||
"dyndns_unavailable": "The domain '{domain}' is unavailable.",
|
||||
"extracting": "Extracting...",
|
||||
"field_invalid": "Invalid field '{}'",
|
||||
|
@ -514,6 +525,7 @@
|
|||
"log_domain_main_domain": "Make '{}' the main domain",
|
||||
"log_domain_remove": "Remove '{}' domain from system configuration",
|
||||
"log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'",
|
||||
"log_dyndns_unsubscribe": "Unsubscribe to a YunoHost subdomain '{}'",
|
||||
"log_dyndns_update": "Update the IP associated with your YunoHost subdomain '{}'",
|
||||
"log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help",
|
||||
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}'",
|
||||
|
|
|
@ -515,10 +515,16 @@ domain:
|
|||
help: Domain name to add
|
||||
extra:
|
||||
pattern: *pattern_domain
|
||||
-d:
|
||||
full: --dyndns
|
||||
help: Subscribe to the DynDNS service
|
||||
--ignore-dyndns:
|
||||
help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service
|
||||
action: store_true
|
||||
--dyndns-recovery-password:
|
||||
metavar: PASSWORD
|
||||
nargs: "?"
|
||||
const: 0
|
||||
help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later delete the domain
|
||||
extra:
|
||||
pattern: *pattern_password
|
||||
|
||||
### domain_remove()
|
||||
remove:
|
||||
|
@ -537,6 +543,16 @@ domain:
|
|||
full: --force
|
||||
help: Do not ask confirmation to remove apps
|
||||
action: store_true
|
||||
--ignore-dyndns:
|
||||
help: If removing a DynDNS domain, only remove the domain, without unsubscribing from the DynDNS service
|
||||
action: store_true
|
||||
--dyndns-recovery-password:
|
||||
metavar: PASSWORD
|
||||
nargs: "?"
|
||||
const: 0
|
||||
help: If removing a DynDNS domain, unsubscribe from the DynDNS service with a password
|
||||
extra:
|
||||
pattern: *pattern_password
|
||||
|
||||
|
||||
### domain_dns_conf()
|
||||
|
@ -623,6 +639,7 @@ domain:
|
|||
path:
|
||||
help: The path to check (e.g. /coffee)
|
||||
|
||||
|
||||
### domain_action_run()
|
||||
action-run:
|
||||
hide_in_help: True
|
||||
|
@ -638,11 +655,65 @@ domain:
|
|||
help: Serialized arguments for action (i.e. "foo=bar&lorem=ipsum")
|
||||
|
||||
subcategories:
|
||||
dyndns:
|
||||
subcategory_help: Subscribe and Update DynDNS Hosts
|
||||
actions:
|
||||
### domain_dyndns_subscribe()
|
||||
subscribe:
|
||||
action_help: Subscribe to a DynDNS service
|
||||
arguments:
|
||||
domain:
|
||||
help: Domain to subscribe to the DynDNS service
|
||||
extra:
|
||||
pattern: *pattern_domain
|
||||
-p:
|
||||
full: --recovery-password
|
||||
nargs: "?"
|
||||
const: 0
|
||||
help: Password used to later recover the domain if needed
|
||||
extra:
|
||||
pattern: *pattern_password
|
||||
|
||||
### domain_dyndns_unsubscribe()
|
||||
unsubscribe:
|
||||
action_help: Unsubscribe from a DynDNS service
|
||||
arguments:
|
||||
domain:
|
||||
help: Domain to unsubscribe from the DynDNS service
|
||||
extra:
|
||||
pattern: *pattern_domain
|
||||
required: True
|
||||
-p:
|
||||
full: --recovery-password
|
||||
nargs: "?"
|
||||
const: 0
|
||||
help: Recovery password used to delete the domain
|
||||
extra:
|
||||
pattern: *pattern_password
|
||||
|
||||
### domain_dyndns_list()
|
||||
list:
|
||||
action_help: List all subscribed DynDNS domains
|
||||
|
||||
### domain_dyndns_set_recovery_password()
|
||||
set-recovery-password:
|
||||
action_help: Set recovery password
|
||||
arguments:
|
||||
domain:
|
||||
help: Domain to set recovery password for
|
||||
extra:
|
||||
pattern: *pattern_domain
|
||||
required: True
|
||||
-p:
|
||||
full: --recovery-password
|
||||
help: The new recovery password
|
||||
extra:
|
||||
password: ask_dyndns_recovery_password
|
||||
pattern: *pattern_password
|
||||
|
||||
config:
|
||||
subcategory_help: Domain settings
|
||||
actions:
|
||||
|
||||
### domain_config_get()
|
||||
get:
|
||||
action_help: Display a domain configuration
|
||||
|
@ -1516,21 +1587,26 @@ firewall:
|
|||
# DynDNS #
|
||||
#############################
|
||||
dyndns:
|
||||
category_help: Subscribe and Update DynDNS Hosts
|
||||
category_help: Subscribe and Update DynDNS Hosts ( deprecated, use 'yunohost domain dyndns' instead )
|
||||
actions:
|
||||
|
||||
### dyndns_subscribe()
|
||||
subscribe:
|
||||
action_help: Subscribe to a DynDNS service
|
||||
deprecated: true
|
||||
arguments:
|
||||
-d:
|
||||
full: --domain
|
||||
help: Full domain to subscribe with
|
||||
help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead )
|
||||
extra:
|
||||
pattern: *pattern_domain
|
||||
-k:
|
||||
full: --key
|
||||
help: Public DNS key
|
||||
-p:
|
||||
full: --recovery-password
|
||||
nargs: "?"
|
||||
const: 0
|
||||
help: Password used to later recover the domain if needed
|
||||
extra:
|
||||
pattern: *pattern_password
|
||||
|
||||
### dyndns_update()
|
||||
update:
|
||||
|
@ -1619,8 +1695,15 @@ tools:
|
|||
required: True
|
||||
comment: good_practices_about_admin_password
|
||||
--ignore-dyndns:
|
||||
help: Do not subscribe domain to a DynDNS service
|
||||
help: If adding a DynDNS domain, only add the domain, without subscribing to the DynDNS service
|
||||
action: store_true
|
||||
--dyndns-recovery-password:
|
||||
metavar: PASSWORD
|
||||
nargs: "?"
|
||||
const: 0
|
||||
help: If adding a DynDNS domain, subscribe to the DynDNS service with a password, used to later recover the domain if needed
|
||||
extra:
|
||||
pattern: *pattern_password
|
||||
--force-diskspace:
|
||||
help: Use this if you really want to install YunoHost on a setup with less than 10 GB on the root filesystem
|
||||
action: store_true
|
||||
|
|
14
src/dns.py
14
src/dns.py
|
@ -640,18 +640,28 @@ def domain_dns_push(operation_logger, domain, dry_run=False, force=False, purge=
|
|||
|
||||
# FIXME: in the future, properly unify this with yunohost dyndns update
|
||||
if registrar == "yunohost":
|
||||
logger.info(m18n.n("domain_dns_registrar_yunohost"))
|
||||
from yunohost.dyndns import dyndns_update
|
||||
dyndns_update(domain=domain, force=force)
|
||||
return {}
|
||||
|
||||
if registrar == "parent_domain":
|
||||
parent_domain = _get_parent_domain_of(domain, topest=True)
|
||||
registar, registrar_credentials = _get_registar_settings(parent_domain)
|
||||
registrar, registrar_credentials = _get_registar_settings(parent_domain)
|
||||
if any(registrar_credentials.values()):
|
||||
raise YunohostValidationError(
|
||||
"domain_dns_push_managed_in_parent_domain",
|
||||
domain=domain,
|
||||
parent_domain=parent_domain,
|
||||
)
|
||||
else:
|
||||
new_parent_domain = ".".join(parent_domain.split(".")[-3:])
|
||||
registrar, registrar_credentials = _get_registar_settings(new_parent_domain)
|
||||
if registrar == "yunohost":
|
||||
raise YunohostValidationError(
|
||||
"domain_dns_push_managed_in_parent_domain",
|
||||
domain=domain,
|
||||
parent_domain=new_parent_domain,
|
||||
)
|
||||
else:
|
||||
raise YunohostValidationError(
|
||||
"domain_registrar_is_not_configured", domain=parent_domain
|
||||
|
|
|
@ -36,6 +36,7 @@ from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_c
|
|||
from yunohost.utils.configpanel import ConfigPanel
|
||||
from yunohost.utils.form import BaseOption
|
||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||
from yunohost.utils.dns import is_yunohost_dyndns_domain
|
||||
from yunohost.log import is_unit_operation
|
||||
|
||||
logger = getActionLogger("yunohost.domain")
|
||||
|
@ -210,21 +211,26 @@ def _get_parent_domain_of(domain, return_self=False, topest=False):
|
|||
return domain if return_self else None
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
def domain_add(operation_logger, domain, dyndns=False):
|
||||
@is_unit_operation(exclude=["dyndns_recovery_password"])
|
||||
def domain_add(operation_logger, domain, dyndns_recovery_password=None, ignore_dyndns=False):
|
||||
"""
|
||||
Create a custom domain
|
||||
|
||||
Keyword argument:
|
||||
domain -- Domain name to add
|
||||
dyndns -- Subscribe to DynDNS
|
||||
|
||||
dyndns_recovery_password -- Password used to later unsubscribe from DynDNS
|
||||
ignore_dyndns -- If we want to just add the DynDNS domain to the list, without subscribing
|
||||
"""
|
||||
from yunohost.hook import hook_callback
|
||||
from yunohost.app import app_ssowatconf
|
||||
from yunohost.utils.ldap import _get_ldap_interface
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
from yunohost.certificate import _certificate_install_selfsigned
|
||||
|
||||
if dyndns_recovery_password:
|
||||
operation_logger.data_to_redact.append(dyndns_recovery_password)
|
||||
|
||||
if domain.startswith("xmpp-upload."):
|
||||
raise YunohostValidationError("domain_cannot_add_xmpp_upload")
|
||||
|
||||
|
@ -245,27 +251,20 @@ def domain_add(operation_logger, domain, dyndns=False):
|
|||
# Non-latin characters (e.g. café.com => xn--caf-dma.com)
|
||||
domain = domain.encode("idna").decode("utf-8")
|
||||
|
||||
# DynDNS domain
|
||||
# Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain )
|
||||
dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3
|
||||
if dyndns:
|
||||
from yunohost.utils.dns import is_yunohost_dyndns_domain
|
||||
from yunohost.dyndns import _guess_current_dyndns_domain
|
||||
|
||||
from yunohost.dyndns import is_subscribing_allowed
|
||||
# Do not allow to subscribe to multiple dyndns domains...
|
||||
if _guess_current_dyndns_domain() != (None, None):
|
||||
if not is_subscribing_allowed():
|
||||
raise YunohostValidationError("domain_dyndns_already_subscribed")
|
||||
|
||||
# Check that this domain can effectively be provided by
|
||||
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
|
||||
if not is_yunohost_dyndns_domain(domain):
|
||||
raise YunohostValidationError("domain_dyndns_root_unknown")
|
||||
if dyndns_recovery_password:
|
||||
assert_password_is_strong_enough("admin", dyndns_recovery_password)
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
if dyndns:
|
||||
from yunohost.dyndns import dyndns_subscribe
|
||||
|
||||
# Actually subscribe
|
||||
dyndns_subscribe(domain=domain)
|
||||
domain_dyndns_subscribe(domain=domain, recovery_password=dyndns_recovery_password)
|
||||
|
||||
_certificate_install_selfsigned([domain], True)
|
||||
|
||||
|
@ -314,8 +313,8 @@ def domain_add(operation_logger, domain, dyndns=False):
|
|||
logger.success(m18n.n("domain_created"))
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
def domain_remove(operation_logger, domain, remove_apps=False, force=False):
|
||||
@is_unit_operation(exclude=["dyndns_recovery_password"])
|
||||
def domain_remove(operation_logger, domain, remove_apps=False, force=False, dyndns_recovery_password=None, ignore_dyndns=False):
|
||||
"""
|
||||
Delete domains
|
||||
|
||||
|
@ -324,12 +323,16 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
|
|||
remove_apps -- Remove applications installed on the domain
|
||||
force -- Force the domain removal and don't not ask confirmation to
|
||||
remove apps if remove_apps is specified
|
||||
|
||||
dyndns_recovery_password -- Recovery password used at the creation of the DynDNS domain
|
||||
ignore_dyndns -- If we just remove the DynDNS domain, without unsubscribing
|
||||
"""
|
||||
from yunohost.hook import hook_callback
|
||||
from yunohost.app import app_ssowatconf, app_info, app_remove
|
||||
from yunohost.utils.ldap import _get_ldap_interface
|
||||
|
||||
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 ...
|
||||
# we don't want to check the domain exists because the ldap add may have
|
||||
# failed
|
||||
|
@ -390,6 +393,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
|
|||
apps="\n".join([x[1] for x in apps_on_that_domain]),
|
||||
)
|
||||
|
||||
# Detect if this is a DynDNS domain ( and not a subdomain of a DynDNS domain )
|
||||
dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain) and len(domain.split(".")) == 3
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
ldap = _get_ldap_interface()
|
||||
|
@ -436,9 +442,59 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
|
|||
|
||||
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"))
|
||||
|
||||
|
||||
def domain_dyndns_subscribe(*args, **kwargs):
|
||||
"""
|
||||
Subscribe to a DynDNS domain
|
||||
"""
|
||||
from yunohost.dyndns import dyndns_subscribe
|
||||
|
||||
dyndns_subscribe(*args, **kwargs)
|
||||
|
||||
|
||||
def domain_dyndns_unsubscribe(*args, **kwargs):
|
||||
"""
|
||||
Unsubscribe from a DynDNS domain
|
||||
"""
|
||||
from yunohost.dyndns import dyndns_unsubscribe
|
||||
|
||||
dyndns_unsubscribe(*args, **kwargs)
|
||||
|
||||
|
||||
def domain_dyndns_list():
|
||||
"""
|
||||
Returns all currently subscribed DynDNS domains
|
||||
"""
|
||||
from yunohost.dyndns import dyndns_list
|
||||
|
||||
return dyndns_list()
|
||||
|
||||
|
||||
def domain_dyndns_update(*args, **kwargs):
|
||||
"""
|
||||
Update a DynDNS domain
|
||||
"""
|
||||
from yunohost.dyndns import dyndns_update
|
||||
|
||||
dyndns_update(*args, **kwargs)
|
||||
|
||||
|
||||
def domain_dyndns_set_recovery_password(*args, **kwargs):
|
||||
"""
|
||||
Set a recovery password for an already registered dyndns domain
|
||||
"""
|
||||
from yunohost.dyndns import dyndns_set_recovery_password
|
||||
|
||||
dyndns_set_recovery_password(*args, **kwargs)
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
def domain_main_domain(operation_logger, new_main_domain=None):
|
||||
"""
|
||||
|
|
230
src/dyndns.py
230
src/dyndns.py
|
@ -17,13 +17,13 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import glob
|
||||
import base64
|
||||
import subprocess
|
||||
import hashlib
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette import Moulinette, m18n
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import write_to_file, rm, chown, chmod
|
||||
|
@ -40,6 +40,17 @@ logger = getActionLogger("yunohost.dyndns")
|
|||
|
||||
DYNDNS_PROVIDER = "dyndns.yunohost.org"
|
||||
DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"]
|
||||
MAX_DYNDNS_DOMAINS = 1
|
||||
|
||||
|
||||
def is_subscribing_allowed():
|
||||
"""
|
||||
Check if the limit of subscribed DynDNS domains has been reached
|
||||
|
||||
Returns:
|
||||
True if the limit is not reached, False otherwise
|
||||
"""
|
||||
return len(dyndns_list()["domains"]) < MAX_DYNDNS_DOMAINS
|
||||
|
||||
|
||||
def _dyndns_available(domain):
|
||||
|
@ -67,23 +78,16 @@ def _dyndns_available(domain):
|
|||
return r == f"Domain {domain} is available"
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
def dyndns_subscribe(operation_logger, domain=None, key=None):
|
||||
@is_unit_operation(exclude=["recovery_password"])
|
||||
def dyndns_subscribe(operation_logger, domain=None, recovery_password=None):
|
||||
"""
|
||||
Subscribe to a DynDNS service
|
||||
|
||||
Keyword argument:
|
||||
domain -- Full domain to subscribe with
|
||||
key -- Public DNS key
|
||||
recovery_password -- Password that will be used to delete the domain
|
||||
"""
|
||||
|
||||
if _guess_current_dyndns_domain() != (None, None):
|
||||
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(
|
||||
|
@ -94,19 +98,40 @@ def dyndns_subscribe(operation_logger, domain=None, key=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
|
||||
)
|
||||
|
||||
if 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
|
||||
# '1234' is idk? doesnt matter, but the old format contained a number here...
|
||||
key_file = f"/etc/yunohost/dyndns/K{domain}.+165+1234.key"
|
||||
|
||||
if key is None:
|
||||
if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0:
|
||||
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
|
||||
|
@ -131,21 +156,24 @@ def dyndns_subscribe(operation_logger, domain=None, key=None):
|
|||
try:
|
||||
# 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 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",
|
||||
data={"subdomain": domain},
|
||||
data=data,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
rm(key_file, force=True)
|
||||
raise YunohostError("dyndns_registration_failed", error=str(e))
|
||||
raise YunohostError("dyndns_subscribe_failed", error=str(e))
|
||||
if r.status_code != 201:
|
||||
rm(key_file, force=True)
|
||||
try:
|
||||
error = json.loads(r.text)["error"]
|
||||
except Exception:
|
||||
error = f'Server error, code: {r.status_code}. (Message: "{r.text}")'
|
||||
raise YunohostError("dyndns_registration_failed", error=error)
|
||||
raise YunohostError("dyndns_subscribe_failed", error=error)
|
||||
|
||||
# Yunohost regen conf will add the dyndns cron job if a key exists
|
||||
# in /etc/yunohost/dyndns
|
||||
|
@ -160,7 +188,124 @@ def dyndns_subscribe(operation_logger, domain=None, key=None):
|
|||
subprocess.check_call(["bash", "-c", cmd.format(t="2 min")])
|
||||
subprocess.check_call(["bash", "-c", cmd.format(t="4 min")])
|
||||
|
||||
logger.success(m18n.n("dyndns_registered"))
|
||||
logger.success(m18n.n("dyndns_subscribed"))
|
||||
|
||||
|
||||
@is_unit_operation(exclude=["recovery_password"])
|
||||
def dyndns_unsubscribe(operation_logger, domain, recovery_password=None):
|
||||
"""
|
||||
Unsubscribe from a DynDNS service
|
||||
|
||||
Keyword argument:
|
||||
domain -- Full domain to unsubscribe with
|
||||
recovery_password -- Password that is used to delete the domain ( defined when subscribing )
|
||||
"""
|
||||
|
||||
import requests # lazy loading this module for performance reasons
|
||||
|
||||
# Unsubscribe the domain using the key if available
|
||||
keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key")
|
||||
if keys:
|
||||
key = keys[0]
|
||||
with open(key) as f:
|
||||
key = f.readline().strip().split(" ", 6)[-1]
|
||||
base64key = base64.b64encode(key.encode()).decode()
|
||||
credential = {"key": base64key}
|
||||
# Otherwise, ask for the recovery password
|
||||
else:
|
||||
if Moulinette.interface.type == "cli" and not recovery_password:
|
||||
logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe"))
|
||||
recovery_password = Moulinette.prompt(
|
||||
m18n.n("ask_dyndns_recovery_password"),
|
||||
is_password=True
|
||||
)
|
||||
|
||||
if not recovery_password:
|
||||
logger.error(f"Cannot unsubscribe the domain {domain}: no credential provided")
|
||||
return
|
||||
|
||||
secret = str(domain) + ":" + str(recovery_password).strip()
|
||||
credential = {"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}
|
||||
|
||||
operation_logger.start()
|
||||
|
||||
# Send delete request
|
||||
try:
|
||||
r = requests.delete(
|
||||
f"https://{DYNDNS_PROVIDER}/domains/{domain}",
|
||||
data=credential,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
raise YunohostError("dyndns_unregistration_failed", error=str(e))
|
||||
|
||||
if r.status_code == 200: # Deletion was successful
|
||||
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"])
|
||||
elif r.status_code == 403:
|
||||
raise YunohostError("dyndns_unsubscribe_denied")
|
||||
elif r.status_code == 409:
|
||||
raise YunohostError("dyndns_unsubscribe_already_unsubscribed")
|
||||
else:
|
||||
raise YunohostError("dyndns_unsubscribe_failed", error=f"The server returned code {r.status_code}")
|
||||
|
||||
logger.success(m18n.n("dyndns_unsubscribed"))
|
||||
|
||||
|
||||
def dyndns_set_recovery_password(domain, recovery_password):
|
||||
|
||||
keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key")
|
||||
|
||||
if not keys:
|
||||
raise YunohostValidationError("dyndns_key_not_found")
|
||||
|
||||
from yunohost.utils.password import assert_password_is_strong_enough
|
||||
assert_password_is_strong_enough("admin", recovery_password)
|
||||
secret = str(domain) + ":" + str(recovery_password).strip()
|
||||
|
||||
key = keys[0]
|
||||
with open(key) as f:
|
||||
key = f.readline().strip().split(" ", 6)[-1]
|
||||
base64key = base64.b64encode(key.encode()).decode()
|
||||
|
||||
import requests # lazy loading this module for performance reasons
|
||||
|
||||
# Send delete request
|
||||
try:
|
||||
r = requests.put(
|
||||
f"https://{DYNDNS_PROVIDER}/domains/{domain}/recovery_password",
|
||||
data={"key": base64key, "recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()},
|
||||
timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
raise YunohostError("dyndns_set_recovery_password_failed", error=str(e))
|
||||
|
||||
if r.status_code == 200:
|
||||
logger.success(m18n.n("dyndns_set_recovery_password_success"))
|
||||
elif r.status_code == 403:
|
||||
raise YunohostError("dyndns_set_recovery_password_denied")
|
||||
elif r.status_code == 404:
|
||||
raise YunohostError("dyndns_set_recovery_password_unknown_domain")
|
||||
elif r.status_code == 409:
|
||||
raise YunohostError("dyndns_set_recovery_password_invalid_password")
|
||||
else:
|
||||
raise YunohostError("dyndns_set_recovery_password_failed", error=f"The server returned code {r.status_code}")
|
||||
|
||||
|
||||
def dyndns_list():
|
||||
"""
|
||||
Returns all currently subscribed DynDNS domains ( deduced from the key files )
|
||||
"""
|
||||
|
||||
from yunohost.domain import domain_list
|
||||
|
||||
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()
|
||||
|
@ -183,16 +328,20 @@ def dyndns_update(
|
|||
import dns.tsigkeyring
|
||||
import dns.update
|
||||
|
||||
# If domain is not given, try to guess it from keys available...
|
||||
key = None
|
||||
# If domain is not given, update all DynDNS domains
|
||||
if domain is None:
|
||||
(domain, key) = _guess_current_dyndns_domain()
|
||||
|
||||
if domain is None:
|
||||
dyndns_domains = dyndns_list()["domains"]
|
||||
|
||||
if not dyndns_domains:
|
||||
raise YunohostValidationError("dyndns_no_domain_registered")
|
||||
|
||||
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
|
||||
elif key is None:
|
||||
keys = glob.glob(f"/etc/yunohost/dyndns/K{domain}.+*.key")
|
||||
|
||||
if not keys:
|
||||
|
@ -330,34 +479,3 @@ def dyndns_update(
|
|||
print(
|
||||
"Warning: dry run, this is only the generated config, it won't be applied"
|
||||
)
|
||||
|
||||
|
||||
def _guess_current_dyndns_domain():
|
||||
"""
|
||||
This function tries to guess which domain should be updated by
|
||||
"dyndns_update()" because there's not proper management of the current
|
||||
dyndns domain :/ (and at the moment the code doesn't support having several
|
||||
dyndns domain, which is sort of a feature so that people don't abuse the
|
||||
dynette...)
|
||||
"""
|
||||
|
||||
DYNDNS_KEY_REGEX = re.compile(r".*/K(?P<domain>[^\s\+]+)\.\+165.+\.key$")
|
||||
|
||||
# Retrieve the first registered domain
|
||||
paths = list(glob.iglob("/etc/yunohost/dyndns/K*.key"))
|
||||
for path in paths:
|
||||
match = DYNDNS_KEY_REGEX.match(path)
|
||||
if not match:
|
||||
continue
|
||||
_domain = match.group("domain")
|
||||
|
||||
# Verify if domain is registered (i.e., if it's available, skip
|
||||
# current domain beause that's not the one we want to update..)
|
||||
# If there's only 1 such key found, then avoid doing the request
|
||||
# for nothing (that's very probably the one we want to find ...)
|
||||
if len(paths) > 1 and _dyndns_available(_domain):
|
||||
continue
|
||||
else:
|
||||
return (_domain, path)
|
||||
|
||||
return (None, None)
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import pytest
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
|
||||
from moulinette.core import MoulinetteError
|
||||
from yunohost.utils.dns import is_yunohost_dyndns_domain
|
||||
|
||||
from yunohost.utils.error import YunohostError, YunohostValidationError
|
||||
from yunohost.domain import (
|
||||
|
@ -16,6 +19,8 @@ from yunohost.domain import (
|
|||
)
|
||||
|
||||
TEST_DOMAINS = ["example.tld", "sub.example.tld", "other-example.com"]
|
||||
TEST_DYNDNS_DOMAIN = "ci-test-" + "".join(chr(random.randint(ord("a"), ord("z"))) for x in range(12)) + random.choice([".noho.st", ".ynh.fr", ".nohost.me"])
|
||||
TEST_DYNDNS_PASSWORD = "astrongandcomplicatedpassphrasethatisverysecure"
|
||||
|
||||
|
||||
def setup_function(function):
|
||||
|
@ -34,9 +39,9 @@ def setup_function(function):
|
|||
|
||||
# Clear other domains
|
||||
for domain in domains:
|
||||
if domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]:
|
||||
if (domain not in TEST_DOMAINS or domain == TEST_DOMAINS[2]) and domain != TEST_DYNDNS_DOMAIN:
|
||||
# Clean domains not used for testing
|
||||
domain_remove(domain)
|
||||
domain_remove(domain, no_unsubscribe=is_yunohost_dyndns_domain(domain))
|
||||
elif domain in TEST_DOMAINS:
|
||||
# Reset settings if any
|
||||
os.system(f"rm -rf {DOMAIN_SETTINGS_DIR}/{domain}.yml")
|
||||
|
@ -65,6 +70,14 @@ def test_domain_add():
|
|||
assert TEST_DOMAINS[2] in domain_list()["domains"]
|
||||
|
||||
|
||||
def test_domain_add_subscribe():
|
||||
|
||||
time.sleep(35) # Dynette blocks requests that happen too frequently
|
||||
assert TEST_DYNDNS_DOMAIN not in domain_list()["domains"]
|
||||
domain_add(TEST_DYNDNS_DOMAIN, dyndns_recovery_password=TEST_DYNDNS_PASSWORD)
|
||||
assert TEST_DYNDNS_DOMAIN in domain_list()["domains"]
|
||||
|
||||
|
||||
def test_domain_add_existing_domain():
|
||||
with pytest.raises(MoulinetteError):
|
||||
assert TEST_DOMAINS[1] in domain_list()["domains"]
|
||||
|
@ -77,6 +90,14 @@ def test_domain_remove():
|
|||
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():
|
||||
current_main_domain = _get_maindomain()
|
||||
assert domain_main_domain()["current_main_domain"] == current_main_domain
|
||||
|
|
21
src/tools.py
21
src/tools.py
|
@ -144,13 +144,14 @@ def _set_hostname(hostname, pretty_hostname=None):
|
|||
logger.debug(out)
|
||||
|
||||
|
||||
@is_unit_operation()
|
||||
@is_unit_operation(exclude=["dyndns_recovery_password", "password"])
|
||||
def tools_postinstall(
|
||||
operation_logger,
|
||||
domain,
|
||||
username,
|
||||
fullname,
|
||||
password,
|
||||
dyndns_recovery_password=None,
|
||||
ignore_dyndns=False,
|
||||
force_diskspace=False,
|
||||
overwrite_root_password=True,
|
||||
|
@ -203,9 +204,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):
|
||||
available = None
|
||||
|
||||
dyndns = not ignore_dyndns and is_yunohost_dyndns_domain(domain)
|
||||
if dyndns:
|
||||
# Check if the domain is available...
|
||||
try:
|
||||
available = _dyndns_available(domain)
|
||||
|
@ -213,17 +213,10 @@ def tools_postinstall(
|
|||
# connectivity or something. Assume that this domain isn't manageable
|
||||
# and inform the user that we could not contact the dyndns host server.
|
||||
except Exception:
|
||||
logger.warning(
|
||||
m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org")
|
||||
)
|
||||
|
||||
if available:
|
||||
dyndns = True
|
||||
# If not, abort the postinstall
|
||||
raise YunohostValidationError("dyndns_provider_unreachable", provider="dyndns.yunohost.org")
|
||||
else:
|
||||
if not available:
|
||||
raise YunohostValidationError("dyndns_unavailable", domain=domain)
|
||||
else:
|
||||
dyndns = False
|
||||
|
||||
if os.system("iptables -V >/dev/null 2>/dev/null") != 0:
|
||||
raise YunohostValidationError(
|
||||
|
@ -235,7 +228,7 @@ def tools_postinstall(
|
|||
logger.info(m18n.n("yunohost_installing"))
|
||||
|
||||
# New domain config
|
||||
domain_add(domain, dyndns)
|
||||
domain_add(domain, dyndns_recovery_password=dyndns_recovery_password, ignore_dyndns=ignore_dyndns)
|
||||
domain_main_domain(domain)
|
||||
|
||||
# First user
|
||||
|
|
Loading…
Add table
Reference in a new issue