From 58614add7905dc6ca361ee79aad66e9d0296c498 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 20:22:40 +0200 Subject: [PATCH] dyndns: add a 'set-recovery-password' command to set the recovery password using only the key --- locales/en.json | 5 +++++ share/actionsmap.yml | 20 ++++++++++++++++++-- src/domain.py | 21 +++++++++++++++------ src/dyndns.py | 45 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index e25911435..bb0976fff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -410,6 +410,11 @@ "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 '{}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index 9132a8545..689c3da86 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -670,7 +670,7 @@ domain: full: --recovery-password nargs: "?" const: 0 - help: Password used to later delete the domain + help: Password used to later recover the domain if needed extra: pattern: *pattern_password @@ -687,7 +687,7 @@ domain: full: --recovery-password nargs: "?" const: 0 - help: Password used to delete the domain + help: Recovery password used to delete the domain extra: pattern: *pattern_password @@ -695,6 +695,22 @@ domain: 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: diff --git a/src/domain.py b/src/domain.py index 8d2758ab2..424b461e8 100644 --- a/src/domain.py +++ b/src/domain.py @@ -450,22 +450,22 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False, dynd logger.success(m18n.n("domain_deleted")) -def domain_dyndns_subscribe(**kwargs): +def domain_dyndns_subscribe(*args, **kwargs): """ Subscribe to a DynDNS domain """ from yunohost.dyndns import dyndns_subscribe - dyndns_subscribe(**kwargs) + dyndns_subscribe(*args, **kwargs) -def domain_dyndns_unsubscribe(**kwargs): +def domain_dyndns_unsubscribe(*args, **kwargs): """ Unsubscribe from a DynDNS domain """ from yunohost.dyndns import dyndns_unsubscribe - dyndns_unsubscribe(**kwargs) + dyndns_unsubscribe(*args, **kwargs) def domain_dyndns_list(): @@ -477,13 +477,22 @@ def domain_dyndns_list(): return dyndns_list() -def domain_dyndns_update(**kwargs): +def domain_dyndns_update(*args, **kwargs): """ Update a DynDNS domain """ from yunohost.dyndns import dyndns_update - dyndns_update(**kwargs) + 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() diff --git a/src/dyndns.py b/src/dyndns.py index 4ed730ecc..dca4e9c77 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -110,7 +110,8 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): is_password=True, confirm=True ) - elif not recovery_password: + + if not recovery_password: logger.warning(m18n.n("dyndns_no_recovery_password")) if recovery_password: @@ -210,8 +211,8 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): key = f.readline().strip().split(" ", 6)[-1] base64key = base64.b64encode(key.encode()).decode() credential = {"key": base64key} + # Otherwise, ask for the recovery password else: - # Ensure sufficiently complex password if Moulinette.interface.type == "cli" and not recovery_password: logger.warning(m18n.n("ask_dyndns_recovery_password_explain_during_unsubscribe")) recovery_password = Moulinette.prompt( @@ -254,6 +255,46 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): 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 )