From cbef40798c2843777a43db4b3b328e1d01abfcda Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Apr 2023 19:40:30 +0200 Subject: [PATCH] dyndns: be able to unsubscribe using the key + domain and i18n string consistency --- locales/en.json | 19 +++++++++-------- share/actionsmap.yml | 10 ++++----- src/dyndns.py | 50 +++++++++++++++++++++++++++++--------------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/locales/en.json b/locales/en.json index b3a1725e8..81d0b8a3e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,7 +91,8 @@ "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 recovey passwory", + "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...", @@ -401,15 +402,15 @@ "dyndns_ip_updated": "Updated your IP on DynDNS", "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_added_password": "Remember your recovery password, you can use it to delete this domain record.", + "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_added_password": "Remember your recovery password, you can use it to delete this domain record.", "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_unregistration_failed": "Could not unregister DynDNS domain: {error}", - "dyndns_unregistered": "DynDNS domain successfully unregistered", - "dyndns_unsubscribe_wrong_password": "Invalid password", - "dyndns_unsubscribe_wrong_domain": "Domain is not registered", + "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_unavailable": "The domain '{domain}' is unavailable.", "extracting": "Extracting...", "field_invalid": "Invalid field '{}'", diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e11073afc..b229be84a 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -663,9 +663,8 @@ domain: subscribe: action_help: Subscribe to a DynDNS service arguments: - -d: - full: --domain - help: Full domain to subscribe with + domain: + help: Domain to subscribe to the DynDNS service extra: pattern: *pattern_domain -p: @@ -681,9 +680,8 @@ domain: unsubscribe: action_help: Unsubscribe from a DynDNS service arguments: - -d: - full: --domain - help: Full domain to unsubscribe with + domain: + help: Domain to unsubscribe from the DynDNS service extra: pattern: *pattern_domain required: True diff --git a/src/dyndns.py b/src/dyndns.py index 2da3de212..4ed730ecc 100644 --- a/src/dyndns.py +++ b/src/dyndns.py @@ -165,14 +165,14 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=None): ) 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 @@ -187,7 +187,7 @@ def dyndns_subscribe(operation_logger, domain=None, recovery_password=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"]) @@ -202,23 +202,37 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): import requests # lazy loading this module for performance reasons - # FIXME : it should be possible to unsubscribe the domain just using the key file ... + # 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} + 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( + m18n.n("ask_dyndns_recovery_password"), + is_password=True + ) - # Ensure sufficiently complex password - if Moulinette.interface.type == "cli" and not recovery_password: - 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: - secret = str(domain) + ":" + str(recovery_password).strip() r = requests.delete( f"https://{DYNDNS_PROVIDER}/domains/{domain}", - data={"recovery_password": hashlib.sha256(secret.encode('utf-8')).hexdigest()}, + data=credential, timeout=30, ) except Exception as e: @@ -230,12 +244,14 @@ def dyndns_unsubscribe(operation_logger, domain, recovery_password=None): # 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: # Wrong password - raise YunohostError("dyndns_unsubscribe_wrong_password") - elif r.status_code == 404: # Invalid domain - raise YunohostError("dyndns_unsubscribe_wrong_domain") + 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_unregistered")) + logger.success(m18n.n("dyndns_unsubscribed")) def dyndns_list():